当前位置: 首页 > news >正文

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

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

相关文章:

  • 几种基于比较的排序
  • 建一个结合双向长短期记忆网络(BiLSTM)和条件随机场(CRF)的模型
  • 【java多线程】线程间通信-利用wait和notify轮流按序打印奇数和偶数
  • 什么是着色器 Shader
  • 正则表达式与文本处理的艺术
  • WPS多级标题编号以及样式控制
  • k6学习k6学习k6学习k6学习k6学习k6学习
  • 【Android】从Choreographer到UI渲染(二)
  • Linux虚拟文件系统(1)
  • Spark,数据提取和保存
  • 数组随机重排与维度转换算法
  • 深入解析Python中的Vector2d类:从基础实现到特殊方法的应用
  • ngx_http_random_index_module 模块概述
  • LoadBarWorks:一款赛博风加载动画生成器的构建旅程
  • linux下的 xargs命令使用详解
  • 墨水屏显示模拟器程序解读
  • jqGrid冻结列错行问题,将冻结表格(悬浮表格)与 正常表格进行高度同步
  • HarmonyOS AVPlayer 音频播放器
  • MyBatis 核心技术详解:从连接池到多表查询
  • 聊天室项目总结
  • 这个死亡率第一的“老年病”,正悄悄逼近年轻人
  • 海外市场,押注中国无人驾驶龙头
  • 俄外长与美国务卿通电话,讨论俄美接触等问题
  • 梅花奖在上海|舞剧《朱鹮》,剧里剧外都是生命的赞歌
  • 人民日报民生观:转人工客服,怎么这么难?
  • 美国调整对华加征关税