Gin + JWT 认证机制详解:构建安全的Go Web应用
前言
在现代Web开发中,用户身份认证是几乎所有应用的核心功能。随着前后端分离架构的普及,基于Token
的认证机制(如JWT
)因其无状态、可扩展性强等特点,逐渐成为主流。本文将详细介绍如何在Go语言的Gin框架中集成JWT(JSON Web Token)实现安全的用户认证。
1. 什么是JWT?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。它通常用于身份验证和信息交换。
一个JWT由三部分组成,用点(.)分隔:
xxxxx.yyyyy.zzzzz
Header(头部):包含令牌类型和使用的签名算法(如HS256)。
Payload(负载):包含声明(claims),如用户ID、过期时间等。
Signature(签名):对前两部分的签名,用于验证消息未被篡改。
https://www.jwt.io/
2. Access Token 和 Refresh Token 的区别
Access Token(访问令牌):
- 用于访问受保护的资源(如 API 接口、用户数据等)。
- 客户端每次向服务器请求资源时,都需要在请求头中携带 access token,服务器验证通过后才允许访问。
- 生命周期短,通常为几分钟到几小时(如 15 分钟、1 小时)
- 可以存储在客户端内存或 localStorage 中(但有 XSS 风险)
Refresh Token(刷新令牌):
- 用于获取新的 access token。
- 当 access token 过期后,客户端可以使用 refresh token 向授权服务器申请一个新的 access token,而无需用户重新登录。
- 生命周期长,可以是几天、几周甚至几个月。但通常只在用户长时间未活动或主动登出时才会失效。
- 必须更安全地存储,如 HTTP Only Cookie、安全的后端存储。
二、集成JWT
1. 安装依赖
go get -u github.com/golang-jwt/jwt/v5
2.配置JWT密钥和过期时间
type Config struct {//...JWT struct {Secret string `mapstructure:"secret"`AccessTokenExpire int `mapstructure:"access_token_expire"` // 单位: 小时RefreshTokenExpire int `mapstructure:"refresh_token_expire"` // 单位: 小时} `mapstructure:"jwt"`
}
3. 创建JWT工具函数
// utils/jwt
import ("context""errors""gin/global""github.com/golang-jwt/jwt/v5""time"
)// 定义 Token 相关常量
const (TokenTypeAccess = "access"TokenTypeRefresh = "refresh"TokenBlacklistPrefix = "jwt:blacklist:"
)// Claims 自定义 JWT 声明
type Claims struct {UserID uint `json:"user_id"`Username string `json:"username"`Type string `json:"type"` // token类型: access 或 refreshjwt.RegisteredClaims
}// GenerateToken 生成 JWT Token
func GenerateToken(userID uint, username string, tokenType string, expireTime time.Duration) (string, error) {// 创建声明claims := Claims{UserID: userID,Username: username,Type: tokenType,RegisteredClaims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireTime)),IssuedAt: jwt.NewNumericDate(time.Now()),NotBefore: jwt.NewNumericDate(time.Now()),},}// 创建 tokentoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)// 签名并获取完整的编码后的字符串 tokentokenString, err := token.SignedString([]byte(global.Config.App.Name))if err != nil {return "", err}return tokenString, nil
}// ParseToken 解析 JWT Token
func ParseToken(tokenString string) (*Claims, error) {if err != nil {return nil, err}if isBlacklisted {return nil, errors.New("token has been blacklisted")}// 解析 tokentoken, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {// 验证签名算法if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {return nil, errors.New("unexpected signing method")}return []byte(global.Config.App.Name), nil})if err != nil {return nil, err}// 提取声明claims, ok := token.Claims.(*Claims)if !ok || !token.Valid {return nil, errors.New("invalid token")}return claims, nil
}// RefreshToken 刷新 Token
func RefreshToken(refreshToken string) (string, string, error) {// 解析 refresh tokenclaims, err := ParseToken(refreshToken)if err != nil {return "", "", err}// 检查是否为 refresh tokenif claims.Type != TokenTypeRefresh {return "", "", errors.New("invalid refresh token")}// 生成新的 access token (有效期短)accessToken, err := GenerateToken(claims.UserID, claims.Username, TokenTypeAccess, time.Hour*time.Duration(global.Config.JWT.AccessTokenExpire))if err != nil {return "", "", err}// 生成新的 refresh token (有效期长)newRefreshToken, err := GenerateToken(claims.UserID, claims.Username, TokenTypeRefresh, time.Hour*time.Duration(global.Config.JWT.RefreshTokenExpire))if err != nil {return "", "", err}return accessToken, newRefreshToken, nil
}
4.创建JWT中间件
// middleware/auth
// JWTAuth 认证中间件
func JWTAuth() gin.HandlerFunc {return func(c *gin.Context) {// 从 Authorization 头中获取 tokenauthHeader := c.GetHeader("Authorization")if authHeader == "" {c.JSON(http.StatusUnauthorized, gin.H{"message": "Authorization header is required"})c.Abort()return}// 检查 token 格式tokenParts := strings.Split(authHeader, " ")if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization format"})c.Abort()return}// 解析 token (ParseToken 函数已经包含了黑名单检查)claims, err := utils.ParseToken(tokenParts[1])if err != nil {c.JSON(http.StatusUnauthorized, gin.H{"message": err.Error()})c.Abort()return}// 检查 token 类型是否为 access tokenif claims.Type != utils.TokenTypeAccess {c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid token type"})c.Abort()return}// 将用户信息存储在上下文中c.Set("userId", claims.UserID)c.Set("username", claims.Username)// 继续处理请求c.Next()}
}
5. 用户路由与认证
// 用户登录
func Login(c *gin.Context) {var req LoginReqerr := c.ShouldBindJSON(&req)if err != nil {ResponseValidateErr(c, err)return}var user models.Usercount := global.DB.Where("username = ?", req.Username).First(&user).RowsAffectedif count == 0 {res.FailWithMessage(c, "用户名或密码错误")return}// 验证密码if !utils.CheckPassword(req.Password, user.Password) {res.FailWithMessage(c, "用户名或密码错误")return}// 生成 access token (24小时过期)accessToken, err := utils.GenerateToken(user.ID, user.Username, utils.TokenTypeAccess, time.Duration(global.Config.JWT.AccessTokenExpire)*time.Hour)if err != nil {res.FailWithMessage(c, "生成Token失败")return}// 生成 refresh token (7天过期)refreshToken, err := utils.GenerateToken(user.ID, user.Username, utils.TokenTypeRefresh, time.Duration(global.Config.JWT.RefreshTokenExpire)*time.Hour)if err != nil {res.FailWithMessage(c, "生成Token失败")return}// 返回 tokenres.Success(c, gin.H{"access_token": accessToken,"refresh_token": refreshToken,"token_type": "Bearer","expires_in": 24 * 3600, // 秒})
}
给受保护的API 增加认证中间件如:
func InitUserRouter(Router *gin.RouterGroup) {userRouter := Router.Group("user"){userRouter.POST("register", api.Register)userRouter.POST("login", api.Login)userRouter.POST("refresh", api.RefreshToken)userRouter.POST("logout",middleware.JWTAuth(), api.Logout)userRouter.GET("list", middleware.JWTAuth(), api.GetUserList)}
}
6.启动服务并测试
登录接口
curl -X POST http://localhost:8080/api/v1/user/login \-H "Content-Type: application/json" \-d '{"username":"user1", "password":"password1"}'
受保护的接口
curl -X GET http://localhost:8080/api/v1/user/list\-H "Authorization: Bearer <your-token>"
三、加入黑名单功能
1. 为什么需要Token黑名单?
JWT的核心优势是无状态,服务器无需存储会话。但这带来了挑战:
无法主动使Token失效:一旦签发,直到过期前都有效。
安全风险:用户登出、密码修改后,旧Token仍可使用。
黑名单机制通过一个中心化存储(如Redis),记录所有已注销但尚未过期的Token,每次请求时检查该列表,从而实现"伪状态化"的注销功能。
2.黑名单管理工具
// BlacklistToken 将 token 加入黑名单
func BlacklistToken(tokenString string, expireTime time.Duration) error {// 获取 token 的过期时间claims, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {return []byte(global.Config.App.Name), nil})if err != nil {// 如果 token 已经过期,我们仍然将其加入黑名单,但设置较短的过期时间if errors.Is(err, jwt.ErrTokenExpired) {// 设置一个默认的过期时间,比如1小时return global.Redis.Set(context.Background(),TokenBlacklistPrefix+tokenString,"blacklisted",time.Hour,).Err()}return err}// 如果 token 有效,获取其过期时间if c, ok := claims.Claims.(*Claims); ok && claims.Valid {// 计算剩余有效期remainingTime := time.Until(c.ExpiresAt.Time)if remainingTime > 0 {expireTime = remainingTime}}// 将 token 加入黑名单,过期时间设为 token 的剩余有效期return global.Redis.Set(context.Background(),TokenBlacklistPrefix+tokenString,"blacklisted",expireTime,).Err()
}// IsTokenBlacklisted 检查 token 是否在黑名单中
func IsTokenBlacklisted(tokenString string) (bool, error) {exists, err := global.Redis.Exists(context.Background(), TokenBlacklistPrefix+tokenString).Result()return exists > 0, err
}
3.增强JWT解析函数
func ParseToken(tokenString string) (*Claims, error) {//1. 检查token是否在黑名单中isBlacklisted, err := IsTokenBlacklisted(tokenString)if err != nil {return nil, err}if isBlacklisted {return nil, errors.New("token has been blacklisted")}//2. 解析 token// ...
}
4.登出接口
func Logout(c *gin.Context) {// 从 Authorization 头中获取 tokenauthHeader := c.GetHeader("Authorization")if authHeader == "" {res.FailWithMessage(c, "Authorization header is required")return}// 检查 token 格式tokenParts := strings.Split(authHeader, " ")if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {res.FailWithMessage(c, "Invalid authorization format")return}// 获取 access tokentokenString := tokenParts[1]// 将 token 加入黑名单,设置过期时间为 access token 的有效期err := utils.BlacklistToken(tokenString, time.Duration(global.Config.JWT.AccessTokenExpire)*time.Hour)if err != nil {res.FailWithMessage(c, "Logout failed")return}// 处理 refresh token 也加入黑名单// ...res.Success(c, nil)
}
5.启动服务测试
四、总结
1 密钥管理
绝不硬编码:生产环境使用环境变量或密钥管理服务。
使用强密钥:至少32字节的随机字符串。
2 Token过期策略
设置合理的过期时间(如15分钟访问Token + 7天刷新Token)。
实现Token刷新机制。
3 防止Token滥用
HTTPS:始终通过HTTPS传输Token。
HttpOnly Cookie:对于Web应用,考虑将Token存储在HttpOnly Cookie中防止XSS攻击。
CORS:正确配置CORS策略。
4 处理Token注销
由于JWT是无状态的,实现注销需要额外机制:
短过期时间 + 刷新Token
Token黑名单:将已注销的Token存入Redis,每次验证时检查。
撤销列表
参考代码
gitee