资讯动态

Gin框架JWT鉴权中间件从零实现token刷新与黑名单机制

发布时间:2026/10/13 9:13:52 来源:尧图企业网站定制
Gin框架JWT鉴权中间件从零实现token刷新与黑名单机制导语在前后端分离架构中JWTJSON Web Token已成为最主流的无状态鉴权方案。Gin作为Go语言中最流行的Web框架如何优雅地集成JWT鉴权、实现token自动刷新、以及构建token黑名单机制是每一个Gin项目必须面对的核心问题。本文将带你从零开始在Gin框架中完整实现一个生产级的JWT鉴权中间件涵盖token签发、自动刷新、黑名单踢人等全套功能。所有代码均经过实战验证可直接用于生产环境。核心技术知识点讲解1. JWT基础结构JWT由三部分组成用.分隔Header.Payload.SignatureHeader指定签名算法HS256/RS256等Payload携带业务数据用户ID、过期时间等Signature对前两部分的签名防止篡改2. Gin中间件原理Gin的中间件本质是gin.HandlerFunc函数通过c.Next()控制请求流向funcAuthMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){// 鉴权逻辑if!validateToken(c){c.AbortWithStatusJSON(401,gin.H{error:unauthorized})return}c.Next()// 放行到下一个处理器}}3. Token刷新策略常见刷新策略有两种策略优点缺点双Token方案AccessRefresh安全性高Refresh Token可设长过期客户端需维护两个Token滑动过期Sliding Expiration用户体验好无感知刷新需维护状态不完全无状态本文采用双Token方案兼顾安全性与用户体验。4. 黑名单机制设计JWT本身是无状态的无法实现踢人下线。引入Redis黑名单在token过期前将其加入黑名单Key: ban:user:{userID}:{jti} TTL: 与token剩余过期时间一致验证时先查黑名单命中则拒绝。实战代码演示/项目案例总结项目结构gin-jwt-demo/ ├── main.go ├── middleware/ │ └── jwt.go ├── handler/ │ └── auth.go ├── config/ │ └── config.go └── go.mod核心代码实现1. 定义JWT工具包middleware/jwt.gopackagemiddlewareimport(crypto/rsaencoding/jsonfmttimegithub.com/gin-gonic/gingithub.com/golang-jwt/jwt/v5github.com/redis/go-redis/v9net/http)var(accessTokenSecret*rsa.PrivateKey refreshTokenSecret*rsa.PrivateKey redisClient*redis.Client)// 初始化密钥生产环境从环境变量或KMS加载funcInitJWT(privateKey*rsa.PrivateKey,pubKey*rsa.PublicKey,rdb*redis.Client){accessTokenSecretprivateKey refreshTokenSecretprivateKey// 实际生产中可用不同密钥redisClientrdb}// Claims 自定义载荷typeClaimsstruct{UserIDint64json:user_idUsernamestringjson:usernameTokenTypestringjson:token_type// access 或 refreshjwt.RegisteredClaims}const(TokenTypeAccessaccessTokenTypeRefreshrefresh)// GenerateTokenPair 生成AccessRefresh双TokenfuncGenerateTokenPair(userIDint64,usernamestring)(accessToken,refreshTokenstring,errerror){now:time.Now()// Access Token15分钟过期accessClaims:Claims{UserID:userID,Username:username,TokenType:TokenTypeAccess,RegisteredClaims:jwt.RegisteredClaims{ID:generateJTI(),// 唯一标识用于黑名单IssuedAt:jwt.NewNumericDate(now),ExpiresAt:jwt.NewNumericDate(now.Add(15*time.Minute)),},}access:jwt.NewWithClaims(jwt.SigningMethodRS256,accessClaims)accessToken,erraccess.SignedString(accessTokenSecret)iferr!nil{return,,fmt.Errorf(生成Access Token失败: %w,err)}// Refresh Token7天过期refreshClaims:Claims{UserID:userID,Username:username,TokenType:TokenTypeRefresh,RegisteredClaims:jwt.RegisteredClaims{ID:generateJTI(),IssuedAt:jwt.NewNumericDate(now),ExpiresAt:jwt.NewNumericDate(now.Add(7*24*time.Hour)),},}refresh:jwt.NewWithClaims(jwt.SigningMethodRS256,refreshClaims)refreshToken,errrefresh.SignedString(refreshTokenSecret)iferr!nil{return,,fmt.Errorf(生成Refresh Token失败: %w,err)}returnaccessToken,refreshToken,nil}// ParseToken 解析并验证TokenfuncParseToken(tokenStringstring,tokenTypestring)(*Claims,error){token,err:jwt.ParseWithClaims(tokenString,Claims{},func(token*jwt.Token)(interface{},error){returnaccessTokenSecret.Public(),nil})iferr!nil{returnnil,fmt.Errorf(Token解析失败: %w,err)}claims,ok:token.Claims.(*Claims)if!ok||!token.Valid{returnnil,fmt.Errorf(Token无效)}// 验证Token类型是否匹配ifclaims.TokenType!tokenType{returnnil,fmt.Errorf(Token类型不匹配期望:%s 实际:%s,tokenType,claims.TokenType)}returnclaims,nil}// AddToBlacklist 将Token加入黑名单踢人下线funcAddToBlacklist(jtistring,userIDint64,expireAt time.Time)error{key:fmt.Sprintf(ban:user:%d:%s,userID,jti)ttl:time.Until(expireAt)ifttl0{returnnil// Token已过期无需加入黑名单}returnredisClient.Set(ctx,key,1,ttl).Err()}// IsInBlacklist 检查Token是否在黑名单中funcIsInBlacklist(jtistring,userIDint64)(bool,error){key:fmt.Sprintf(ban:user:%d:%s,userID,jti)exists,err:redisClient.Exists(ctx,key).Result()returnexists0,err}// 生成唯一JTIJWT IDfuncgenerateJTI()string{returnfmt.Sprintf(%d-%s,time.Now().UnixNano(),randomString(8))}2. JWT鉴权中间件// JWTAuthMiddleware Gin鉴权中间件funcJWTAuthMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){// 1. 从Header提取TokenauthHeader:c.GetHeader(Authorization)ifauthHeader{c.AbortWithStatusJSON(http.StatusUnauthorized,gin.H{code:401,message:缺少Authorization Header,})return}// 2. 解析 Bearer Tokenparts:strings.SplitN(authHeader, ,2)iflen(parts)!2||parts[0]!Bearer{c.AbortWithStatusJSON(http.StatusUnauthorized,gin.H{code:401,message:Authorization格式错误应为: Bearer {token},})return}// 3. 解析并验证Tokenclaims,err:ParseToken(parts[1],TokenTypeAccess)iferr!nil{c.AbortWithStatusJSON(http.StatusUnauthorized,gin.H{code:401,message:fmt.Sprintf(Token无效: %v,err),})return}// 4. 检查黑名单banned,err:IsInBlacklist(claims.ID,claims.UserID)iferr!nil{c.AbortWithStatusJSON(http.StatusInternalServerError,gin.H{code:500,message:鉴权服务异常,})return}ifbanned{c.AbortWithStatusJSON(http.StatusUnauthorized,gin.H{code:401,message:Token已被吊销请重新登录,})return}// 5. 将用户信息注入上下文c.Set(userID,claims.UserID)c.Set(username,claims.Username)c.Set(jti,claims.ID)c.Next()}}3. Token刷新接口handler/auth.gopackagehandlerimport(github.com/gin-gonic/ginnet/http)typeRefreshRequeststruct{RefreshTokenstringjson:refresh_token binding:required}typeAuthResponsestruct{AccessTokenstringjson:access_tokenRefreshTokenstringjson:refresh_tokenExpiresInint64json:expires_in// 秒}// RefreshToken 刷新Access TokenfuncRefreshToken(c*gin.Context){varreq RefreshRequestiferr:c.ShouldBindJSON(req);err!nil{c.JSON(http.StatusBadRequest,gin.H{code:400,message:参数错误: err.Error(),})return}// 解析Refresh Tokenclaims,err:middleware.ParseToken(req.RefreshToken,middleware.TokenTypeRefresh)iferr!nil{c.JSON(http.StatusUnauthorized,gin.H{code:401,message:Refresh Token无效或已过期,})return}// 检查Refresh Token是否在黑名单用户主动退出登录banned,_:middleware.IsInBlacklist(claims.ID,claims.UserID)ifbanned{c.JSON(http.StatusUnauthorized,gin.H{code:401,message:Refresh Token已被吊销,})return}// 生成新的Token对accessToken,refreshToken,err:middleware.GenerateTokenPair(claims.UserID,claims.Username)iferr!nil{c.JSON(http.StatusInternalServerError,gin.H{code:500,message:生成Token失败,})return}// 将旧的Refresh Token加入黑名单一次性使用_middleware.AddToBlacklist(claims.ID,claims.UserID,claims.ExpiresAt.Time)c.JSON(http.StatusOK,gin.H{code:0,message:刷新成功,data:AuthResponse{AccessToken:accessToken,RefreshToken:refreshToken,ExpiresIn:15*60,// 15分钟},})}// Logout 退出登录将当前Token加入黑名单funcLogout(c*gin.Context){jti,_:c.Get(jti)userID,_:c.Get(userID)exp,_:c.Get(exp)// 需从claims中提取过期时间// 将当前Access Token和Refresh Token都加入黑名单// 实际实现中需前端同时传递Refresh Token_middleware.AddToBlacklist(jti.(string),userID.(int64),exp.(time.Time))c.JSON(http.StatusOK,gin.H{code:0,message:退出登录成功,})}4. 主程序注册路由main.gopackagemainimport(crypto/rsagithub.com/gin-gonic/gingithub.com/redis/go-redis/v9gin-jwt-demo/handlergin-jwt-demo/middleware)funcmain(){// 1. 加载RSA密钥示例使用gin内置的密钥生成privateKey,_:rsa.GenerateKey(rand.Reader,2048)publicKey:privateKey.PublicKey// 2. 初始化Redisrdb:redis.NewClient(redis.Options{Addr:localhost:6379,})// 3. 初始化JWTmiddleware.InitJWT(privateKey,publicKey,rdb)// 4. 注册路由r:gin.Default()// 公开路由public:r.Group(/api/v1){public.POST(/login,handler.Login)public.POST(/refresh,handler.RefreshToken)}// 受保护路由protected:r.Group(/api/v1)protected.Use(middleware.JWTAuthMiddleware()){protected.GET(/profile,handler.GetProfile)protected.POST(/logout,handler.Logout)}r.Run(:8080)}开发痛点与报错避坑指南坑1crypto/rsa密钥加载失败报错信息panic: crypto/rsa: invalid key原因直接从字符串加载PEM格式密钥时未正确使用bytes.NewBuffer或Decode。正确做法// 从文件加载私钥privateKeyBytes,_:os.ReadFile(private_key.pem)block,_:pem.Decode(privateKeyBytes)privateKey,err:x509.ParsePKCS1PrivateKey(block.Bytes)生产环境建议使用环境变量或密钥管理服务如AWS KMS、阿里云KMS。坑2Claims类型断言失败报错信息interface conversion: *jwt.Token is not *middleware.Claims原因ParseWithClaims传入的Claims实例类型必须与后续类型断言一致。正确做法// 正确传入指针类型claims:Claims{}token,err:jwt.ParseWithClaims(tokenString,claims,keyFunc)// 直接使用claims无需类型断言坑3中间件中使用c.Set后c.Get返回nil原因在中间件调用c.Set()后若在c.Next()之后获取值需注意c.Copy()的上下文隔离。正确做法// 在中间件中设置c.Set(userID,userID)c.Next()// 在后续处理器中通过c.Get(userID)获取而非c.Copy()坑4Redis黑名单Key过期时间计算错误问题Token剩余有效期计算错误导致黑名单Key提前过期失去拦截效果。正确做法// 计算Token剩余TTLttl:time.Until(claims.ExpiresAt.Time)ifttl0{redisClient.Set(ctx,key,1,ttl)}坑5Refresh Token被盗用导致安全漏洞问题Refresh Token若被截获攻击者可无限刷新。解决方案Refresh Token设置一次性使用使用后立即加入黑名单记录Refresh Token的使用IP和设备指纹支持用户查看并吊销所有活跃会话全文总结技术进阶展望本文从零实现了一个生产级GinJWT鉴权方案核心要点总结双Token设计Access Token短过期保障安全Refresh Token长过期保障体验RSA签名相比HS256RSA支持密钥分发适合微服务场景黑名单机制通过Redis实现token吊销弥补JWT无状态缺陷中间件封装统一的鉴权中间件对业务代码零侵入进阶方向引入gin-jwt开源库https://github.com/appleboy/gin-jwt减少重复造轮子结合go-oauth2实现OAuth2.0完整授权流程使用pasetoPlatform-Agnostic Security Tokens替代JWT避免算法混淆漏洞在网关层如Kong/APISIX统一处理鉴权应用层只关注业务逻辑参考文献JWT官方规范https://tools.ietf.org/html/rfc7519golang-jwt/jwt库https://github.com/golang-jwt/jwtGin中间件文档https://gin-gonic.com/zh-cn/docs/examples/middleware/OWASP JWT安全备忘单https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.htmlRedis官方文档https://redis.io/docs/RSA密钥管理最佳实践https://crypto.stackexchange.com/questions/81512/

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑