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})
    })
}

相关推荐
她说彩礼65万2 分钟前
C# 中的锁
开发语言·c#
2302_809798323 分钟前
【JavaWeb】JDBC
java·开发语言·servlet
繁依Fanyi21 分钟前
ImgShrink:摄影暗房里的在线图片压缩工具开发记
开发语言·前端·codebuddy首席试玩官
卓律涤27 分钟前
【找工作系列①】【大四毕业】【复习】巩固JavaScript,了解ES6。
开发语言·前端·javascript·笔记·程序人生·职场和发展·es6
xqlily38 分钟前
MATLAB安装常见问题解决方案
开发语言·matlab
achene_ql42 分钟前
基于QT和FFmpeg实现自己的视频播放器FFMediaPlayer(一)——项目总览
开发语言·qt·ffmpeg
MeyrlNotFound1 小时前
(二十一)Java集合框架源码深度解析
java·开发语言
TNTLWT1 小时前
Qt功能区:Ribbon使用
开发语言·qt·ribbon
Ronin3051 小时前
【C++】18.二叉搜索树
开发语言·数据结构·c++
Susea&1 小时前
初始C++:类和对象(中)
c语言·开发语言·c++