Go 后端中双 token 的实现模板

下面是一个典型的 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})
    })
}

相关推荐
程序员小假6 分钟前
我们来说一下 b+ 树与 b 树的区别
java·后端
阿贵---24 分钟前
C++中的RAII技术深入
开发语言·c++·算法
Traced back30 分钟前
怎么用 Modbus 让两个设备互相通信**,包含硬件接线、协议原理、读写步骤,以及 C# 实操示例。
开发语言·c#
Meepo_haha38 分钟前
Spring Boot 条件注解:@ConditionalOnProperty 完全解析
java·spring boot·后端
sheji34161 小时前
【开题答辩全过程】以 基于springboot的房屋租赁系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Victor3562 小时前
MongoDB(57)如何优化MongoDB的查询性能?
后端
Victor3562 小时前
MongoDB(58)如何使用索引优化查询?
后端
行百里er2 小时前
优雅应对异常,从“try-catch堆砌”到“设计驱动”
java·后端·代码规范
娇娇yyyyyy2 小时前
QT编程(17): Qt 实现自定义列表模型
开发语言·qt