JWT 认证中间件设计
Access/Refresh 双令牌、签名校验、中间件注入上下文与吊销策略;和 go-auth-and-jwt 概念篇配合的工程设计笔记。
#type / concept
#status / growing
#tech / dev / backend
#resource / go
[!info] 关联笔记
JWT 认证中间件设计
这个概念为什么出现
仅“会用 jwt 库签名”不够。服务要回答:
- Access 短命如何刷新?
- 中间件如何把身份注入后续 handler?
- 注销如何尽快失效?
- 算法与密钥如何防踩坑?
本篇聚焦中间件与双令牌流程;基础 JWT 结构见 go-auth-and-jwt。
[!abstract] 一句话理解 校验 Bearer access token → 解析 Claims → 写入 context;refresh 换发新 access;吊销靠 TTL 黑名单或版本号。
双 Token 策略
| Token | TTL 经验 | 存放 | 用途 |
|---|---|---|---|
| Access | 短(分钟~小时级,按威胁模型) | 内存/严格存储 | API 鉴权 |
| Refresh | 长 | HttpOnly Cookie 或安全存储 + 服务端可吊销记录 | 换 access |
表中“7 天 access”等具体数字仅作旧笔记示例;生产应按风险缩短 access TTL。
Claims 设计
type Claims struct {
UserID string `json:"uid"`
jwt.RegisteredClaims
}
exp/iat/nbf用 RegisteredClaimsjti(JWT ID)便于黑名单- 不要塞权限全集过大或敏感 PII
库常用 golang-jwt/jwt(社区)。
中间件骨架(net/http)
type ctxKey int
const userIDKey ctxKey = 1
func UserIDFrom(ctx context.Context) (string, bool) {
v, ok := ctx.Value(userIDKey).(string)
return v, ok
}
func AuthMiddleware(secret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := r.Header.Get("Authorization")
if !strings.HasPrefix(h, "Bearer ") {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
raw := strings.TrimPrefix(h, "Bearer ")
claims, err := parseAndValidate(raw, secret)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// 可选:Redis 黑名单 Exists(jti)
ctx := context.WithValue(r.Context(), userIDKey, claims.UserID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
校验要点
- 强制算法:
jwt.WithValidMethods([]string{"HS256"})或非对称套件,防 alg=none / 算法混淆 - 密钥:HS* 用足够长随机;RS*/ES* 管私钥
- 时钟偏移:少量
Leeway - HTTPS 传输
刷新流程
客户端过期 → POST /auth/refresh + refresh token
服务端验 refresh(签名+存储会话/轮换)
→ 签发新 access(可选轮换 refresh)
Refresh 轮换:每次使用作废旧 refresh,防重放。
吊销
| 方案 | 说明 |
|---|---|
| 短 access + 等过期 | 简单,窗口内仍有效 |
| jti 黑名单(Redis TTL) | 注销立即生效 |
| token 版本号存用户表 | 改密后全局失效 |
与分层
- 中间件:认证(你是谁)
- Service:鉴权(你能否做)
- 不要在 JWT 里只信客户端角色字段而不校验服务端权威数据(敏感权限)
边界
- JWT 不是加密,是签名;载荷可见。
- 吊销与无状态之间的张力——完全无状态难以立即登出。
- 移动端与 Web 存储策略不同(XSS/CSRF)。
常见误区
[!warning] 从 token 解析后不校验签名
等同明文。
[!warning] 密钥写死仓库
用配置/密钥管理。
[!warning] access TTL 过长又无吊销
泄露窗口过大。
工程实践
- 统一 401 与 WWW-Authenticate 策略
- 审计登录/刷新/注销
- 限流登录与刷新接口
- 表驱动测:过期、错误签名、错误 alg、黑名单命中
- Gin 版本只是把
c.Request.Context()换适配,核心不变
自测题
- 为何 access 要短、refresh 要可吊销?
- 中间件应把什么放入 context?
- alg 混淆攻击防什么?
答案
- 降低泄露窗口;长会话可服务端作废。
- 已验证的稳定身份标识(user id),而非整段原始 token(除非审计需要)。
- 攻击者改 alg 用公钥当 HMAC 密钥等;强制预期算法。
延伸阅读
笔记元信息
- 文件:
jwt-auth-middleware-go.md - 状态:已深化