下面是一个典型的 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
)
go
package utils
import (
"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 := accessSecret
if 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
)
go
package middleware
import (
"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
)
go
package redis
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
var RDB *redis.Client
func 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
)
go
package controller
import (
"chat/redis"
"chat/utils"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func Login(c *gin.Context) {
// 假设账号密码校验通过,用户ID是 123
userID := 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
)
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})
})
}