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

Gin + JWT 认证机制详解:构建安全的Go Web应用

前言

在现代Web开发中,用户身份认证是几乎所有应用的核心功能。随着前后端分离架构的普及,基于Token的认证机制(如JWT)因其无状态可扩展性强等特点,逐渐成为主流。本文将详细介绍如何在Go语言的Gin框架中集成JWT(JSON Web Token)实现安全的用户认证。

1. 什么是JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。它通常用于身份验证和信息交换。

一个JWT由三部分组成,用点(.)分隔:

xxxxx.yyyyy.zzzzz

Header(头部):包含令牌类型和使用的签名算法(如HS256)。
Payload(负载):包含声明(claims),如用户ID、过期时间等。
Signature(签名):对前两部分的签名,用于验证消息未被篡改。
https://www.jwt.io/

2. Access Token 和 Refresh Token 的区别
Access Token(访问令牌):
  • 用于访问受保护的资源(如 API 接口、用户数据等)。
  • 客户端每次向服务器请求资源时,都需要在请求头中携带 access token,服务器验证通过后才允许访问。
  • 生命周期短,通常为几分钟到几小时(如 15 分钟、1 小时)
  • 可以存储在客户端内存或 localStorage 中(但有 XSS 风险)
Refresh Token(刷新令牌):
  • 用于获取新的 access token。
  • 当 access token 过期后,客户端可以使用 refresh token 向授权服务器申请一个新的 access token,而无需用户重新登录。
  • 生命周期长,可以是几天、几周甚至几个月。但通常只在用户长时间未活动或主动登出时才会失效。
  • 必须更安全地存储,如 HTTP Only Cookie、安全的后端存储。

二、集成JWT

1. 安装依赖
go get -u github.com/golang-jwt/jwt/v5
2.配置JWT密钥和过期时间
type Config struct {//...JWT struct {Secret             string `mapstructure:"secret"`AccessTokenExpire  int    `mapstructure:"access_token_expire"`  // 单位: 小时RefreshTokenExpire int    `mapstructure:"refresh_token_expire"` // 单位: 小时} `mapstructure:"jwt"`
}
3. 创建JWT工具函数
// utils/jwt
import ("context""errors""gin/global""github.com/golang-jwt/jwt/v5""time"
)// 定义 Token 相关常量
const (TokenTypeAccess      = "access"TokenTypeRefresh     = "refresh"TokenBlacklistPrefix = "jwt:blacklist:"
)// Claims 自定义 JWT 声明
type Claims struct {UserID   uint   `json:"user_id"`Username string `json:"username"`Type     string `json:"type"` // token类型: access 或 refreshjwt.RegisteredClaims
}// GenerateToken 生成 JWT Token
func GenerateToken(userID uint, username string, tokenType string, expireTime time.Duration) (string, error) {// 创建声明claims := Claims{UserID:   userID,Username: username,Type:     tokenType,RegisteredClaims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireTime)),IssuedAt:  jwt.NewNumericDate(time.Now()),NotBefore: jwt.NewNumericDate(time.Now()),},}// 创建 tokentoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)// 签名并获取完整的编码后的字符串 tokentokenString, err := token.SignedString([]byte(global.Config.App.Name))if err != nil {return "", err}return tokenString, nil
}// ParseToken 解析 JWT Token
func ParseToken(tokenString string) (*Claims, error) {if err != nil {return nil, err}if isBlacklisted {return nil, errors.New("token has been blacklisted")}// 解析 tokentoken, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {// 验证签名算法if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {return nil, errors.New("unexpected signing method")}return []byte(global.Config.App.Name), nil})if err != nil {return nil, err}// 提取声明claims, ok := token.Claims.(*Claims)if !ok || !token.Valid {return nil, errors.New("invalid token")}return claims, nil
}// RefreshToken 刷新 Token
func RefreshToken(refreshToken string) (string, string, error) {// 解析 refresh tokenclaims, err := ParseToken(refreshToken)if err != nil {return "", "", err}// 检查是否为 refresh tokenif claims.Type != TokenTypeRefresh {return "", "", errors.New("invalid refresh token")}// 生成新的 access token (有效期短)accessToken, err := GenerateToken(claims.UserID, claims.Username, TokenTypeAccess, time.Hour*time.Duration(global.Config.JWT.AccessTokenExpire))if err != nil {return "", "", err}// 生成新的 refresh token (有效期长)newRefreshToken, err := GenerateToken(claims.UserID, claims.Username, TokenTypeRefresh, time.Hour*time.Duration(global.Config.JWT.RefreshTokenExpire))if err != nil {return "", "", err}return accessToken, newRefreshToken, nil
}
4.创建JWT中间件
// middleware/auth
// JWTAuth 认证中间件
func JWTAuth() gin.HandlerFunc {return func(c *gin.Context) {// 从 Authorization 头中获取 tokenauthHeader := c.GetHeader("Authorization")if authHeader == "" {c.JSON(http.StatusUnauthorized, gin.H{"message": "Authorization header is required"})c.Abort()return}// 检查 token 格式tokenParts := strings.Split(authHeader, " ")if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization format"})c.Abort()return}// 解析 token (ParseToken 函数已经包含了黑名单检查)claims, err := utils.ParseToken(tokenParts[1])if err != nil {c.JSON(http.StatusUnauthorized, gin.H{"message": err.Error()})c.Abort()return}// 检查 token 类型是否为 access tokenif claims.Type != utils.TokenTypeAccess {c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid token type"})c.Abort()return}// 将用户信息存储在上下文中c.Set("userId", claims.UserID)c.Set("username", claims.Username)// 继续处理请求c.Next()}
}
5. 用户路由与认证
// 用户登录
func Login(c *gin.Context) {var req LoginReqerr := c.ShouldBindJSON(&req)if err != nil {ResponseValidateErr(c, err)return}var user models.Usercount := global.DB.Where("username = ?", req.Username).First(&user).RowsAffectedif count == 0 {res.FailWithMessage(c, "用户名或密码错误")return}// 验证密码if !utils.CheckPassword(req.Password, user.Password) {res.FailWithMessage(c, "用户名或密码错误")return}// 生成 access token (24小时过期)accessToken, err := utils.GenerateToken(user.ID, user.Username, utils.TokenTypeAccess, time.Duration(global.Config.JWT.AccessTokenExpire)*time.Hour)if err != nil {res.FailWithMessage(c, "生成Token失败")return}// 生成 refresh token (7天过期)refreshToken, err := utils.GenerateToken(user.ID, user.Username, utils.TokenTypeRefresh, time.Duration(global.Config.JWT.RefreshTokenExpire)*time.Hour)if err != nil {res.FailWithMessage(c, "生成Token失败")return}// 返回 tokenres.Success(c, gin.H{"access_token":  accessToken,"refresh_token": refreshToken,"token_type":    "Bearer","expires_in":    24 * 3600, // 秒})
}

给受保护的API 增加认证中间件如:

func InitUserRouter(Router *gin.RouterGroup) {userRouter := Router.Group("user"){userRouter.POST("register", api.Register)userRouter.POST("login", api.Login)userRouter.POST("refresh", api.RefreshToken)userRouter.POST("logout",middleware.JWTAuth(), api.Logout)userRouter.GET("list", middleware.JWTAuth(), api.GetUserList)}
}
6.启动服务并测试

登录接口

curl -X POST http://localhost:8080/api/v1/user/login \-H "Content-Type: application/json" \-d '{"username":"user1", "password":"password1"}'

受保护的接口

curl -X GET http://localhost:8080/api/v1/user/list\-H "Authorization: Bearer <your-token>"

三、加入黑名单功能

1. 为什么需要Token黑名单?

JWT的核心优势是无状态,服务器无需存储会话。但这带来了挑战:
无法主动使Token失效:一旦签发,直到过期前都有效。
安全风险:用户登出、密码修改后,旧Token仍可使用。
黑名单机制通过一个中心化存储(如Redis),记录所有已注销但尚未过期的Token,每次请求时检查该列表,从而实现"伪状态化"的注销功能。

2.黑名单管理工具
// BlacklistToken 将 token 加入黑名单
func BlacklistToken(tokenString string, expireTime time.Duration) error {// 获取 token 的过期时间claims, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {return []byte(global.Config.App.Name), nil})if err != nil {// 如果 token 已经过期,我们仍然将其加入黑名单,但设置较短的过期时间if errors.Is(err, jwt.ErrTokenExpired) {// 设置一个默认的过期时间,比如1小时return global.Redis.Set(context.Background(),TokenBlacklistPrefix+tokenString,"blacklisted",time.Hour,).Err()}return err}// 如果 token 有效,获取其过期时间if c, ok := claims.Claims.(*Claims); ok && claims.Valid {// 计算剩余有效期remainingTime := time.Until(c.ExpiresAt.Time)if remainingTime > 0 {expireTime = remainingTime}}// 将 token 加入黑名单,过期时间设为 token 的剩余有效期return global.Redis.Set(context.Background(),TokenBlacklistPrefix+tokenString,"blacklisted",expireTime,).Err()
}// IsTokenBlacklisted 检查 token 是否在黑名单中
func IsTokenBlacklisted(tokenString string) (bool, error) {exists, err := global.Redis.Exists(context.Background(), TokenBlacklistPrefix+tokenString).Result()return exists > 0, err
}
3.增强JWT解析函数
func ParseToken(tokenString string) (*Claims, error) {//1. 检查token是否在黑名单中isBlacklisted, err := IsTokenBlacklisted(tokenString)if err != nil {return nil, err}if isBlacklisted {return nil, errors.New("token has been blacklisted")}//2. 解析 token// ...
}
4.登出接口
func Logout(c *gin.Context) {// 从 Authorization 头中获取 tokenauthHeader := c.GetHeader("Authorization")if authHeader == "" {res.FailWithMessage(c, "Authorization header is required")return}// 检查 token 格式tokenParts := strings.Split(authHeader, " ")if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {res.FailWithMessage(c, "Invalid authorization format")return}// 获取 access tokentokenString := tokenParts[1]// 将 token 加入黑名单,设置过期时间为 access token 的有效期err := utils.BlacklistToken(tokenString, time.Duration(global.Config.JWT.AccessTokenExpire)*time.Hour)if err != nil {res.FailWithMessage(c, "Logout failed")return}// 处理 refresh token 也加入黑名单// ...res.Success(c, nil)
}
5.启动服务测试

四、总结

1 密钥管理

绝不硬编码:生产环境使用环境变量或密钥管理服务。
使用强密钥:至少32字节的随机字符串。

2 Token过期策略

设置合理的过期时间(如15分钟访问Token + 7天刷新Token)。
实现Token刷新机制。

3 防止Token滥用

HTTPS:始终通过HTTPS传输Token。
HttpOnly Cookie:对于Web应用,考虑将Token存储在HttpOnly Cookie中防止XSS攻击。
CORS:正确配置CORS策略。

4 处理Token注销

由于JWT是无状态的,实现注销需要额外机制:

短过期时间 + 刷新Token
Token黑名单:将已注销的Token存入Redis,每次验证时检查。
撤销列表

参考代码

gitee

http://www.dtcms.com/a/405792.html

相关文章:

  • VS要求的.NET 9 SDK需求、安装注意事项及VS版本搭配
  • SSL证书安全的几个问题
  • 4种安全方法:将一加手机联系人传输到电脑
  • 网站建设盐城北安网站建设
  • Guidde:AI驱动的视频文档创建工具
  • 29.Linux防火墙管理
  • 记账本|基于SSM的家庭记账本小程序设计与实现(源码+数据库+文档)
  • DHCP 服务器
  • K8s学习笔记(七) yaml
  • K8S的StorageClass使用节点本地LVM逻辑卷怎么进行PVC扩容
  • 软件开发公司如何通过 UI 设计服务打造差异化竞争力
  • 【源码剖析】5-生产者-RecordAccumulator分析
  • PHP编程基础
  • 单片机 | 基于51单片机的摇摇棒设计全解析
  • 从零开始部署Android环境的Jenkins CI/CD流水线(docker环境,Win系统)
  • HttpSessionBindingListener
  • AndroidEventBus 发布者发布一次订阅者接收到多次问题
  • Unity开发CI/CD工具Jenkins的安装(Windows10)
  • 按键精灵安卓/ios辅助工具,脚本开发新手教程ui界面介绍
  • Machine Learning HW4 report: 语者识别 (Hongyi Lee)
  • Android 系统源码级进程保活全方案:从进程创建到后台防护
  • 在hadoop中Job提交的流程
  • 基于Qt和FFmpeg的安卓监控模拟器/手机摄像头模拟成onvif和28181设备
  • 01MemoryOS环境搭建 python3.10
  • 建设部网站职责划定html精美登录界面源码
  • 网站建设基本步骤顺序网站的整体风格
  • Leetcode 146. LRU 缓存 哈希表 + 双向链表
  • VideollaMA 3论文阅读
  • Android 14 系统 ANR (Application Not Responding) 深度分析与解决指南
  • 《红色脉络:一部PLMN在中国的演进史诗 (1G-6G)》 第11篇 | 核心网演进终局:从EPC到5GC——微服务与“云原生”