登录态是几乎每个后端都要做的。JWT 把用户信息塞进 token 本身,服务端不存会话,适合多实例部署。这一篇把签发、校验、刷新三件事都跑通。
1. 定义 Claims
用 github.com/golang-jwt/jwt/v5。Claims 里放用户 ID 和过期时间,外加标准字段:
type Claims struct { UserID uint `json:"uid"` jwt.RegisteredClaims } var jwtSecret = []byte("从环境变量读,别硬编码") // 生产用 32 字节以上的随机串
RegisteredClaims 已经带了 ExpiresAt、IssuedAt,直接用。
2. 登录签发 token
登录接口校验密码后生成 token。过期时间设 2 小时:
func (h *UserHandler) Login(c *gin.Context) { var req LoginReq if err := c.ShouldBindJSON(&req); err != nil { Fail(c, CodeParamErr, err.Error()) return } u, err := h.svc.VerifyPassword(req.Username, req.Password) if err != nil { FailErr(c, &BizError{Code: CodeUnauthorized, Msg: "账号或密码错误"}) return } claims := Claims{ UserID: u.ID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString(jwtSecret) if err != nil { FailErr(c, err) return } OK(c, gin.H{"token": signed}) }
签名算法用 HS256(对称密钥)。多服务需要互相验签时换 RS256(非对称),私钥签名、公钥验。
3. 校验中间件
每个受保护接口前挂 AuthRequired,解析 token 并塞进 context:
func AuthRequired() gin.HandlerFunc { return func(c *gin.Context) { header := c.GetHeader("Authorization") if !strings.HasPrefix(header, "Bearer ") { Fail(c, CodeUnauthorized, "未登录") c.Abort() return } tokenStr := strings.TrimPrefix(header, "Bearer ") claims := &Claims{} _, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { return jwtSecret, nil }) if err != nil { Fail(c, CodeUnauthorized, "登录失效") c.Abort() return } c.Set("uid", claims.UserID) c.Next() } }
ParseWithClaims 会顺带校验签名和过期时间,过期直接返回错误。解析失败就返回 401,handler 里 c.Get("uid") 拿到用户 ID。
4. 接进分层
把中间件挂到需要登录的路由组,handler 从 context 取 uid:
auth := v1.Group("/users", AuthRequired()) { auth.GET("/me", userHandler.Me) } func (h *UserHandler) Me(c *gin.Context) { uid, _ := c.Get("uid") u, err := h.svc.Get(uid.(uint)) if err != nil { FailErr(c, err) return } OK(c, u) }
5. 刷新 token
访问令牌短、刷新令牌长。登录时同时发一个 7 天有效的 refresh token,过期前用它换新的 access token:
func (h *UserHandler) Refresh(c *gin.Context) { var req struct{ Refresh string `json:"refresh" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { Fail(c, CodeParamErr, err.Error()) return } claims := &Claims{} if _, err := jwt.ParseWithClaims(req.Refresh, claims, keyFunc); err != nil { Fail(c, CodeUnauthorized, "refresh 失效") c.Abort() return } newToken, _ := signToken(claims.UserID) // 复用签发逻辑 OK(c, gin.H{"token": newToken}) }
刷新令牌要能主动作废(用户改密码、踢人),单 JWT 做不到,需要服务端存一个 token 版本号或加黑名单。简单做法是用户表加 token_version 字段,Claims 里带上,校验时比对当前值,不一致即失效。
6. 几个上线前必查的点
密钥从环境变量读,提交代码前确认没写死在源文件里。访问令牌有效期别超过 2 小时。HTTPS 必须开,否则 token 在传输中被截获就等于明文密码。Authorization 头用 Bearer 前缀,前端照此拼接。
下一篇接数据库,把 h.svc.Get 背后的 UserRepo 换成真实的 GORM 查询。