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

相关推荐
快乐的哈士奇3 分钟前
对话框打字机效果:Vur + Java/Python 实现
java·开发语言·python
ch.ju9 分钟前
Java程序设计(第3版)第四章——类的组成
java·开发语言
我命由我123459 分钟前
PHP - PHP 基本随机数生成函数
开发语言·ide·后端·java-ee·php·intellij-idea·intellij idea
博.闻广见10 分钟前
AI_Python基础-4.标准库与IO
开发语言·python
吃好睡好便好11 分钟前
在Matlab中绘制质点运动轨迹图
开发语言·学习·算法·matlab·信息可视化
richard_yuu12 分钟前
C#开发全景概述:从零读懂C#的定位、优势与完整技术体系
开发语言·c#
Xin_ye1008613 分钟前
C# 零基础到精通教程 - 第十二章:异常处理与调试——让程序更健壮
开发语言·c#
楼田莉子15 分钟前
C#学习之C#入门学习
开发语言·后端·学习·c#
我命由我1234516 分钟前
PHP - PHP 简易 Web 服务器、基础接口开发
服务器·开发语言·前端·php·intellij-idea·idea·intellij idea
Reload.17 分钟前
CZ航司,shopping JS逆向 acw_sc__v2
开发语言·javascript·python·网络爬虫·ecmascript