JWT解析不应重复执行,应在中间件中解析一次并存入context供后续中间件复用;KeyFunc须内存缓存密钥、禁止IO;黑名单检查需结合jti判断、Redis TTL对齐及本地LRU缓存;静态资源等非敏感路径应跳过JWT校验。

JWT解析不能在每次请求里重复做
echo-jwt/v4 默认每次请求都调用 jwt.ParseWithClaims,但解析本身含 Base64 解码、签名验算、时间字段校验三步,CPU 开销明显。实测单核 QPS 超 3000 后,这部分就成瓶颈。
解决办法是复用已解析的 token:在 JWT 中间件里把 *jwt.Token 实例存进 context(用 c.Set("jwt_token", token)),后续 RBAC 或日志中间件直接 c.Get("jwt_token") 类型断言取用,跳过二次解析。
- 别在 RBAC 中间件里再调
jwt.Parse—— 它已经存在了 - 避免在日志中间件里读
c.Get("user")后又手动解析 raw token 做审计,除非你真需要原始字符串 - 如果你用的是自定义 claims 结构体,确保它实现了
jwt.Claims接口,否则token.Claims断言会 panic
KeyFunc 必须快,且禁止阻塞
echojwt.Config.KeyFunc 是每次请求必调的函数,很多人在这里查数据库或 Redis 拿密钥,结果 RT 直接从 0.5ms 拉到 20ms+,并发一上来就雪崩。
正确做法是预加载密钥并内存缓存:
- HS256 场景:启动时读一次配置或环境变量,转成
[]byte全局变量,KeyFunc直接返回它 - RS256 场景:启动时用
rsa.PublicKey解析 PEM,缓存指针;不要每次请求都ParsePKIXPublicKey - 绝对不要在
KeyFunc里做网络 I/O 或加锁互斥 —— 它运行在每个请求 goroutine 里
黑名单检查必须带短路和 TTL 对齐
加黑名单本意是登出即时失效,但查 Redis 本身有延迟和失败风险。如果每请求都走一次 GET revoked:{jti},Redis 成为新的性能瓶颈,且网络抖动会导致大量 500。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
优化要点:
- 只对带
jti的 token 查黑名单;签发时没设jti的,跳过这步(用token.Header["jti"] != nil判断) - Redis key 的 TTL 必须和 token 的
exp对齐(比如 exp 是 900 秒,key 就SET revoked:{jti} "" EX 900 NX),避免无效扫描 - 加一层本地 LRU 缓存(如
bigcache),缓存「已确认 revoked」的 jti,10 秒 TTL,命中直接拒绝,不打 Redis
别让 JWT 中间件覆盖所有路由
静态资源、健康检查、OpenAPI 文档等路径根本不需要 JWT 校验,但很多人图省事写 e.Use(echojwt.New(...)),结果每个 /favicon.ico 都跑一遍签名验证。
用 Skipper 精确放行:
Skipper: func(c echo.Context) bool { return strings.HasPrefix(c.Request().URL.Path, "/static/") || c.Request().URL.Path == "/health" }- 或者更彻底:不用全局
e.Use(),改用路由级注册,如e.GET("/api/user", handler, jwtMw),只给真正需要的路由加 - 注意
e.Static()内置的文件服务自动跳过中间件链,比手写GET("/static/*", staticHandler)更安全
真实高并发场景下,JWT 校验的耗时差异不在算法本身,而在你是否让解析、密钥加载、黑名单查询这三件事脱离了请求热路径。最容易被忽略的是 KeyFunc 的 IO 隐患——它看着像一个回调,实际是每个请求的必经入口。









