
# JWT认证与授权:完整实现方案摘要:JWT无状态认证完整实现涵盖JWT结构原理、Go jwt库签发与验证、认证中间件、Token刷新机制附时钟偏移踩坑经验。团队从单体拆成微服务后认证方案要重新设计。原来用Session存Redis每个服务都要查一次Redis验证用户身份。微服务一多Redis查询量翻了好几倍每个服务还要配Redis连接维护成本高。后来改用JWT服务端签发Token后不需要存储每个服务自己验证签名就行去掉了Redis依赖。但JWT也有它的问题今天就聊聊完整的实现方案。JWT结构原理JWT由三部分组成用点号分隔格式是Header.Payload.Signature。Header指定签名算法Payload存放用户数据Signature是对前两部分的签名。三部分做Base64URL编码后拼接成最终字符串。Payload只是Base64编码任何人都能解码千万别放密码这类敏感信息。签发与验证Go里用github.com/golang-jwt/jwt/v5库处理JWT。packagemainimport(fmttimegithub.com/golang-jwt/jwt/v5)// JWT密钥生产环境从配置读取varsecretKey[]byte(my-secret-key-change-in-production)// Claims定义JWT的载荷结构typeClaimsstruct{UserIDint64json:user_id// 用户IDUsernamestringjson:username// 用户名Rolestringjson:role// 用户角色jwt.RegisteredClaims// 嵌入标准声明}// GenerateToken签发JWT TokenfuncGenerateToken(userIDint64,username,rolestring)(string,error){claims:Claims{UserID:userID,Username:username,Role:role,RegisteredClaims:jwt.RegisteredClaims{ExpiresAt:jwt.NewNumericDate(time.Now().Add(2*time.Hour)),// 2小时过期IssuedAt:jwt.NewNumericDate(time.Now()),Issuer:my-app,},}token:jwt.NewWithClaims(jwt.SigningMethodHS256,claims)returntoken.SignedString(secretKey)// 用密钥签名}// ParseToken验证并解析JWT TokenfuncParseToken(tokenStringstring)(*Claims,error){claims:Claims{}token,err:jwt.ParseWithClaims(tokenString,claims,func(token*jwt.Token)(interface{},error){// 验证签名算法必须是HS256if_,ok:token.Method.(*jwt.SigningMethodHMAC);!ok{returnnil,fmt.Errorf(意外的签名算法)}returnsecretKey,nil})iferr!nil||!token.Valid{returnnil,fmt.Errorf(Token无效)}returnclaims,nil}认证中间件JWT验证逻辑放在中间件里统一拦截这里用Gin演示。packagemainimport(net/httpstringsgithub.com/gin-gonic/gin)// AuthMiddleware 认证中间件funcAuthMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){// 从Authorization头获取TokenauthHeader:c.GetHeader(Authorization)parts:strings.SplitN(authHeader, ,2)iflen(parts)!2||parts[0]!Bearer{c.AbortWithStatusJSON(http.StatusUnauthorized,gin.H{error:认证格式错误})return}// 验证Tokenclaims,err:ParseToken(parts[1])iferr!nil{c.AbortWithStatusJSON(http.StatusUnauthorized,gin.H{error:Token无效或已过期})return}// 用户信息存入Contextc.Set(user_id,claims.UserID)c.Set(role,claims.Role)c.Next()}}// AdminMiddleware 角色权限校验funcAdminMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){role,_:c.Get(role)ifrole!admin{c.AbortWithStatusJSON(http.StatusForbidden,gin.H{error:需要管理员权限})return}c.Next()}}funcmain(){r:gin.Default()// 登录接口r.POST(/login,func(c*gin.Context){token,_:GenerateToken(123,zhangsan,admin)c.JSON(http.StatusOK,gin.H{token:token})})// 认证路由组auth:r.Group(/api).Use(AuthMiddleware()){auth.GET(/profile,func(c*gin.Context){c.JSON(http.StatusOK,gin.H{user_id:c.GetInt64(user_id)})})// 管理员路由auth.Group(/admin).Use(AdminMiddleware()).GET(/users,func(c*gin.Context){c.JSON(http.StatusOK,gin.H{msg:用户列表})})}r.Run(:8080)}Token刷新机制JWT签发后无法撤回泄露后过期前一直有效。方案是短有效期加Refresh Token。AccessToken 15分钟过期RefreshToken 7天过期且只用于换取AccessToken。// GenerateAccessToken 15分钟访问TokenfuncGenerateAccessToken(userIDint64,username,rolestring)(string,error){claims:Claims{UserID:userID,Username:username,Role:role,RegisteredClaims:jwt.RegisteredClaims{ExpiresAt:jwt.NewNumericDate(time.Now().Add(15*time.Minute)),IssuedAt:jwt.NewNumericDate(time.Now()),},}returnjwt.NewWithClaims(jwt.SigningMethodHS256,claims).SignedString(secretKey)}// GenerateRefreshToken 7天刷新Token不携带业务数据funcGenerateRefreshToken(userIDint64)(string,error){claims:jwt.RegisteredClaims{Subject:fmt.Sprintf(%d,userID),// 只存用户IDExpiresAt:jwt.NewNumericDate(time.Now().Add(7*24*time.Hour)),IssuedAt:jwt.NewNumericDate(time.Now()),}returnjwt.NewWithClaims(jwt.SigningMethodHS256,claims).SignedString(secretKey)}AccessToken泄露最多影响15分钟RefreshToken泄露可在服务端加黑名单使其失效。独家踩坑:时钟偏移导致认证失败上线后偶尔有用户反馈登录后马上Token过期复现概率5%。怀疑前端问题查了半天没发现异常。报错请求都来自同一台服务器。有一台NTP同步出了问题时钟快了90秒。JWT验证时当前时间超过exp就判过期时钟快90秒Token就提前过期有效期边界的请求就偶发性失败。双管齐下修NTP同步确保时钟一致验证时加偏移容忍。// 修复允许60秒偏差funcParseTokenWithLeeway(tokenStringstring)(*Claims,error){claims:Claims{}// 通过Parser设置leeway容忍偏差parser:jwt.NewParser(jwt.WithValidMethods([]string{HS256}),jwt.WithLeeway(60*time.Second),// 允许60秒偏差)token,err:parser.ParseWithClaims(tokenString,claims,func(t*jwt.Token)(interface{},error){returnsecretKey,nil})iferr!nil||!token.Valid{returnnil,fmt.Errorf(Token无效)}returnclaims,nil}jwt.WithLeeway(60*time.Second)让时间检查容忍60秒偏差生产环境建议设30到60秒。对比分析方案状态存储跨服务撤回能力性能SessionRedis服务端存储需共享Redis强直接删Session中JWT无状态天然支持弱需黑名单高Session Cookie服务端存储单域名强中JWT最大的优势是无状态每个服务独立验证Token不依赖共享存储代价是撤回困难。Java Spring Security内置刷新和黑名单但配置复杂。Go的golang-jwt库更轻量高级功能自己实现。总结JWT适合微服务无状态认证核心三件事签发、验证、刷新。时钟偏移这个坑很常见生产环境配NTP同步加leeway。下一篇聊OAuth2.0登录。