下面是一个典型的 Go 后端双 Token 认证机制 实现模板,使用 Gin 框架 + JWT + Redis,结构清晰、可拓展,适合实战开发。
项目结构建议
/utils├── jwt.go // Access & Refresh token 的生成和解析├── claims.go // 从请求中提取用户信息
/middleware└── auth.go // 中间件:校验 Access Token + 黑名单
/controller├── auth.go // 登录、刷新、登出接口
/redis└── client.go // Redis 黑名单管理
1. JWT 工具(utils/jwt.go
)
package utilsimport ("time""github.com/golang-jwt/jwt/v5"
)var accessSecret = []byte("access_secret")
var refreshSecret = []byte("refresh_secret")type CustomClaims struct {UserID uint `json:"user_id"`jwt.RegisteredClaims
}func GenerateAccessToken(userID uint) (string, error) {claims := CustomClaims{UserID: userID,RegisteredClaims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),IssuedAt: jwt.NewNumericDate(time.Now()),},}return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(accessSecret)
}func GenerateRefreshToken(userID uint) (string, error) {claims := CustomClaims{UserID: userID,RegisteredClaims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),IssuedAt: jwt.NewNumericDate(time.Now()),},}return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(refreshSecret)
}func ParseToken(tokenStr string, isRefresh bool) (*CustomClaims, error) {key := accessSecretif isRefresh {key = refreshSecret}token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {return key, nil})if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {return claims, nil}return nil, err
}
2. 中间件验证 Access Token(middleware/auth.go
)
package middlewareimport ("chat/redis""chat/utils""github.com/gin-gonic/gin""net/http""strings"
)func JWTAuthMiddleware() gin.HandlerFunc {return func(c *gin.Context) {authHeader := c.GetHeader("Authorization")if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {c.JSON(http.StatusUnauthorized, gin.H{"error": "token required"})c.Abort()return}token := strings.TrimPrefix(authHeader, "Bearer ")claims, err := utils.ParseToken(token, false)if err != nil {c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})c.Abort()return}if redis.IsBlacklisted(token) {c.JSON(http.StatusUnauthorized, gin.H{"error": "token is blacklisted"})c.Abort()return}c.Set("userID", claims.UserID)c.Next()}
}
3. Redis 黑名单管理(redis/client.go
)
package redisimport ("context""time""github.com/redis/go-redis/v9"
)var RDB *redis.Clientfunc InitRedis() {RDB = redis.NewClient(&redis.Options{Addr: "localhost:6379",DB: 0,})
}var ctx = context.Background()func AddToBlacklist(token string, expiration time.Duration) {RDB.Set(ctx, "blacklist:"+token, "1", expiration)
}func IsBlacklisted(token string) bool {val, err := RDB.Get(ctx, "blacklist:"+token).Result()return err == nil && val == "1"
}
4. 登录、刷新、退出接口(controller/auth.go
)
package controllerimport ("chat/redis""chat/utils""github.com/gin-gonic/gin""net/http""time"
)func Login(c *gin.Context) {// 假设账号密码校验通过,用户ID是 123userID := uint(123)accessToken, _ := utils.GenerateAccessToken(userID)refreshToken, _ := utils.GenerateRefreshToken(userID)c.SetCookie("refresh_token", refreshToken, 7*24*3600, "/", "localhost", false, true)c.JSON(http.StatusOK, gin.H{"access_token": accessToken})
}func Refresh(c *gin.Context) {refreshToken, err := c.Cookie("refresh_token")if err != nil {c.JSON(http.StatusUnauthorized, gin.H{"error": "refresh token not found"})return}claims, err := utils.ParseToken(refreshToken, true)if err != nil {c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})return}newAccessToken, _ := utils.GenerateAccessToken(claims.UserID)c.JSON(http.StatusOK, gin.H{"access_token": newAccessToken})
}func Logout(c *gin.Context) {token := c.GetHeader("Authorization")if token != "" {token = token[len("Bearer "):]redis.AddToBlacklist(token, 15*time.Minute) // 过期时间与 access token 一致}c.SetCookie("refresh_token", "", -1, "/", "localhost", false, true)c.JSON(http.StatusOK, gin.H{"message": "logout success"})
}
5. 路由注册(main.go
)
r := gin.Default()
redis.InitRedis()auth := r.Group("/auth")
{auth.POST("/login", controller.Login)auth.POST("/refresh", controller.Refresh)auth.POST("/logout", controller.Logout)
}api := r.Group("/api", middleware.JWTAuthMiddleware())
{api.GET("/me", func(c *gin.Context) {userID := c.GetUint("userID")c.JSON(http.StatusOK, gin.H{"user_id": userID})})
}