为什么要 JWT 鉴权
没有鉴权的问题
假设 /api/user/info 返回用户敏感信息:
GET /api/user/info?uid=1
我们根据上篇Login实现的测试截图便可以看到,登录成功后服务器会以明文会返回大量用户信息
❌ 任何人都能查任意用户的信息
❌ 恶意用户可以 遍历 uid 拿到所有用户数据
❌ 无法知道请求是谁发的
有 JWT 鉴权后
POST /api/user/login → 返回 token
GET /api/user/info
Headers: Authorization: Bearer eyJhbGciOiJI...
✅ 只有登录后的用户才能访问
✅ 服务端通过 token 知道是谁在请求
✅ token 有时效,过期了需要重新登录
类比
| 场景 | 现实 | 接口 |
|---|---|---|
| 无Token | 随便进景区 | 随便调接口 |
| 有Token | 门票+身份证 | token + 用户身份 |
业务需求
秒杀系统的核心流程:
用户登录 → 获取Token → 秒杀请求(带Token) → 验证Token → 扣库存
后续的秒杀、订单等接口都必须知道"是谁在请求",JWT 就是解决这个问题的。
一. 安装依赖并配置JWTConfig
1) 安装JWT依赖
go get -u github.com/golang-jwt/jwt/v5
2) 添加JWTConfig结构体
来到config/config.go
// JWTConfig 映射yaml文件中的jwt配置
type JWTConfig struct {Secret string `mapstructure:"secret"` // JWT签名密钥Expire int `mapstructure:"expire"` // JWT过期时间
}// Config 是整个项目的配置树
type Config struct {Server ServerConfig `mapstructure:"server"` // 服务器相关配置Database DatabaseConfig `mapstructure:"database"` // 数据库相关配置JWT JWTConfig `mapstructure:"jwt"` // (新增)JWT相关配置
}
3) 在config.yaml中更新jwt配置项
config/config.yaml
jwt:secret: "your-secret-key-change-in-production" # 替换成你自己的JWT密钥expire: 7200 # 2小时
这里如果不知道怎么生成密钥的话可以到免费JWT密钥生成器 | 安全的HS256、HS384、HS512密钥在线生成
默认的256即可
二. 添加JWT工具(生成与解析)
创建pkg/jwtx/jwt.go
package jwtximport ("time""github.com/golang-jwt/jwt/v5"
)// Claims 定义JWT的自定义声明结构体,包含用户ID和用户名
type Claims struct {Uid uint `json:"uid"` // 用户IDUsername string `json:"username"` // 用户名jwt.RegisteredClaims // 继承标准的JWT注册声明
}// GenerateToken 生成JWT token,接受用户ID、用户名、密钥和过期时间作为参数,返回生成的JWT字符串或错误
func GenerateToken(uid uint, username, secrect string, expire int) (string, error) {claims := Claims{Uid: uid,Username: username,RegisteredClaims: jwt.RegisteredClaims{ // 继承标准的JWT注册声明ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expire) * time.Second),), // 设置过期时间IssuedAt: jwt.NewNumericDate(time.Now()), // 设置签发时间},}token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // 创建一个新的JWT token,使用HS256签名方法,并将claims作为负载return token.SignedString([]byte(secrect)) // 使用提供的密钥对token进行签名,并返回生成的JWT字符串
}// ParseToken 解析JWT token,接受JWT字符串和密钥作为参数,返回解析后的Claims结构体或错误
func ParseToken(tokenString, secrect string) (*Claims, error) {token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {return []byte(secrect), nil // 提供密钥用于验证token的签名})if err != nil {return nil, err}if claims, ok := token.Claims.(*Claims); ok && token.Valid { // 验证token的有效性,并将claims断言为自定义的Claims类型return claims, nil}return nil, jwt.ErrSignatureInvalid // 如果token无效或签名不正确,返回签名无效错误
}
三. 鉴权中间件
创建internal/middleware/auto.go
我们需要在这里实现Token的解析,并利用上方jwt.go写好的解析方法完成鉴权
package middlewareimport ("net/http""strings""github.com/Chuan81/secgo-mall/config""github.com/Chuan81/secgo-mall/pkg/jwtx""github.com/gin-gonic/gin"
)// JWTAuth 是一个Gin中间件函数,用于验证请求中的JWT token是否合法
func JWTAuth() gin.HandlerFunc {return func(c *gin.Context) {authHeader := c.GetHeader("Authorization") // 从请求头中获取Authorization字段的值// 如果Authorization字段为空,返回401 Unauthorized错误if authHeader == "" {c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "missing token"})return}parts := strings.SplitN(authHeader, " ", 2) // 将Authorization字段的值按照空格分割成两部分,第一部分应该是"Bearer",第二部分是实际的JWT token// 如果分割后的部分数量不等于2,或者第一部分不是"Bearer",则返回401 Unauthorized错误if len(parts) != 2 || parts[0] != "Bearer" {c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid token format"})return}claims, err := jwtx.ParseToken(parts[1], config.GlobalConfig.JWT.Secret) // 调用jwtx包中的ParseToken函数,传入JWT token和配置中的JWT签名密钥进行解析// 如果解析过程中发生错误,返回401 Unauthorized错误if err != nil {c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid token: " + err.Error()})return}c.Set("uid", claims.Uid) // 将解析后的用户ID存储在Gin的上下文中,供后续处理函数使用c.Set("username", claims.Username) // 将解析后的用户名存储在Gin的上下文中,供后续处理函数使用c.Next() // 继续处理请求,调用下一个中间件或处理函数}
}
四. Login方法的更新-生成并返回Token
1) 更新service
internal/service/user.go
我们这里要对service.Login()方法进行更新,使其能够生成JWT并返回
func Login(req *dto.LoginRequest) (*model.User, string, error) {// 根据用户名查询用户user, err := repository.GetUserByUsername(req.Username)if err != nil {return nil, "", errors.New("User not found")}// bcrypt验证密码if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {return nil, "", errors.New("invalid password")}// 传参生成JWT tokentoken, err := jwtx.GenerateToken(user.Uid, user.Username, config.GlobalConfig.JWT.Secret, config.GlobalConfig.JWT.Expire)if err != nil {return nil, "", errors.New("failed to generate token")}return user, token, nil
}
2) 更新handlers
internal/api/handlers/user.go
这里接着更新对应的handlers.Login()方法
func Login(c *gin.Context) {var req dto.LoginRequest// 绑定请求参数到DTO结构体,并进行验证if err := c.ShouldBindJSON(&req); err != nil {response.Fail(c, 400, "invalid request parameters: "+err.Error())return}// 调用服务层的登录逻辑user, token, err := service.Login(&req)if err != nil {response.Fail(c, 401, "login failed: "+err.Error())return}// 登录成功,返回用户信息(不包含密码)response.Sucess(c, gin.H{"uid": user.Uid,"username": user.Username,"token": token,})
}
五. 鉴权路由
cmd/secgo-mall/main.go
func main() {// ... 现有代码 ...r := gin.Default()// 无需鉴权的公共路由r.POST("/api/user/register", handlers.Register)r.POST("/api/user/login", handlers.Login)// 需要鉴权的私有路由protected := r.Group("/api")protected.Use(middleware.JWTAuth()) // 使用JWT鉴权中间件{// 在这里定义需要鉴权的路由protected.GET("/user/info", func(c *gin.Context) {uid, _ := c.Get("uid")username, _ := c.Get("username")response.Sucess(c, gin.H{"uid": uid,"username": username,})})}// ... 现有代码 ...
六. Postman 测试流程
1) 登录获取Token
POST http://localhost:8080/api/user/login
Body: { "username": "admin", "password": "admin123" }

2) 带Token访问需鉴权接口
在请求头添加:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsIn...
GET http://localhost:8080/api/user/info

