← 返回博客
go2026-09-12 20:30:172 分钟 · 492 0

Gin JWT 鉴权实战:登录签发、中间件校验、刷新

用 jwt-go 签发带自定义 Claims 的 token,写登录接口,做 Auth 中间件解析校验,再补一个刷新 token 的流程。

#gin#JWT#鉴权

登录态是几乎每个后端都要做的。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 已经带了 ExpiresAtIssuedAt,直接用。

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 查询。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/182