Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!

简介: 本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。

在现代 Web 应用中,JWT(JSON Web Token)已经成为了主流的认证与授权解决方案。它轻量、高效、易于实现,并且非常适合于微服务架构。

在本文中,我们将通过 Go 语言及其流行的 Gin 框架,来深入探讨如何使用 JWT 实现用户认证和安全保护。

什么是 JWT?

JSON Web Tokens(JWT)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明。JWT是一个紧凑、URL安全的方式,用于在双方之间传递信息。在认证流程中,JWT被用来验证用户身份,并传递用户状态信息。

其结构主要包括三部分:

  • Header:包含令牌的类型和签名算法。
  • Payload:携带用户信息(如用户 ID)和一些标准声明(如签发者、过期时间等)。
  • Signature:用来验证令牌的真实性,防止被篡改。

JWT 的魅力在于它是自包含的,可以通过令牌直接获取用户信息,而无需在服务器端维护会话状态。

使用 Gin 和 JWT 实现用户认证

让我们从实际代码开始,演示如何在 Gin 中集成 JWT 认证。

package main

import (
    "log"
    "strings"
    "time"

    "github.com/davecgh/go-spew/spew"
    "github.com/gin-gonic/gin"
    jwtPkg "github.com/golang-jwt/jwt/v4"
    "github.com/pkg/errors"
)

在上述代码中,我们首先导入了必要的包,包括用于处理 JWT 的 github.com/golang-jwt/jwt/v4 包和用于错误处理的 github.com/pkg/errors 包。

JWT 结构体定义

var (
    ErrTokenGenFailed         = errors.New("令牌生成失败")
    ErrTokenExpired           = errors.New("令牌已过期")
    ErrTokenExpiredMaxRefresh = errors.New("令牌已过最大刷新时间")
    ErrTokenMalformed         = errors.New("请求令牌格式有误")
    ErrTokenInvalid           = errors.New("请求令牌无效")
    ErrTokenNotFound          = errors.New("无法找到令牌")
)

// JWT 定义一个 jwt 对象
type JWT struct {
   
    Key        []byte // 密钥
    MaxRefresh int64  // 最大刷新时间(分钟)
    ExpireTime int64  // 过期时间(分钟)
    Issuer     string // 签发者
}

JWT 结构体包含了实现 JWT 所需的关键信息,如密钥、最大刷新时间、过期时间和签发者信息。我们使用这些字段来配置和管理 JWT。

生成 JWT

func NewJWT(secret, issuer string, maxRefreshTime, expireTime int64) *JWT {
   
    if maxRefreshTime <= expireTime {
   
        log.Fatal("最大刷新时间必须大于 token 的过期时间")
    }

    return &JWT{
   
        Key:        []byte(secret), // 密钥
        MaxRefresh: maxRefreshTime, // 允许刷新时间
        ExpireTime: expireTime,     // token 过期时间
        Issuer:     issuer,         // token 的签发者
    }
}

通过 NewJWT 方法,我们可以创建一个 JWT 实例。这个实例将用于生成、解析和刷新 JWT。需要注意的是,最大刷新时间必须大于 token 的过期时间,否则会导致逻辑错误。

解析 JWT

func (j *JWT) ParseToken(c *gin.Context, userToken ...string) (*JWTCustomClaims, error) {
   
    var (
        tokenStr string
        err      error
    )

    if len(userToken) > 0 {
   
        tokenStr = userToken[0]
    } else {
   
        tokenStr, err = j.GetToken(c)
        if err != nil {
   
            return nil, err
        }
    }

    token, err := j.parseTokenString(tokenStr)

    if err != nil {
   
        validationErr, ok := err.(*jwtPkg.ValidationError)
        if ok {
   
            switch validationErr.Errors {
   
            case jwtPkg.ValidationErrorMalformed:
                return nil, ErrTokenMalformed
            case jwtPkg.ValidationErrorExpired:
                return nil, ErrTokenExpired
            }
        }
        return nil, ErrTokenInvalid
    }

    if claims, ok := token.Claims.(*JWTCustomClaims); ok && token.Valid {
   
        return claims, nil
    }

    return nil, ErrTokenInvalid
}

ParseToken 方法用于解析 JWT 并验证其有效性。如果令牌无效或者过期,会返回相应的错误信息。这个方法是我们在各个需要鉴权的 API 接口中最常用的一个方法。

刷新 JWT

func (j *JWT) RefreshToken(c *gin.Context) (string, error) {
   
    tokenStr, err := j.GetToken(c)
    if err != nil {
   
        return "", err
    }

    token, err := j.parseTokenString(tokenStr)

    if err != nil {
   
        validationErr, ok := err.(*jwtPkg.ValidationError)
        if !ok || validationErr.Errors != jwtPkg.ValidationErrorExpired {
   
            return "", err
        }
    }

    claims := token.Claims.(*JWTCustomClaims)
    maxRefreshTime := time.Duration(j.MaxRefresh) * time.Minute

    if claims.IssuedAt > time.Now().Add(-maxRefreshTime).Unix() {
   
        claims.StandardClaims.ExpiresAt = j.expireAtTime()
        return j.createToken(*claims)
    }

    return "", ErrTokenExpiredMaxRefresh
}

RefreshToken 方法允许在 token 过期但仍在允许刷新时间内时,重新生成一个新的 token。这对于长时间需要保持登录状态的应用非常有用。

这只是刷新 token 的一种思路,还有一种思路也可以刷新 token,但是就需要用到两个 token,一个 access_token 和 refresh_token ,这里我直接将代码贴进来,大家可以参考参考。

package main

import (
    "log"
    "time"

    jwtPkg "github.com/golang-jwt/jwt/v4"
)

type ARJWT struct {
   
    // 密钥,用以加密 JWT
    Key []byte

    // 定义 access token 过期时间(单位:分钟)即当颁发 access token 后,多少分钟后 access token 过期
    AccessExpireTime int64

    // 定义 refresh token 过期时间(单位:分钟)即当颁发 refresh token 后,多少分钟后 refresh token 过期
    // 一般来说,refresh token 的过期时间会比 access token 的过期时间长
    RefreshExpireTime int64

    // token 的签发者
    Issuer string
}

func NewARJWT(secret, issuer string, accessExpireTime, refreshExpireTime int64) *ARJWT {
   
    if refreshExpireTime <= accessExpireTime {
   
        log.Fatal("refresh token 过期时间必须大于 access token 过期时间")
    }
    return &ARJWT{
   
        Key:               []byte(secret),    // 密钥
        AccessExpireTime:  accessExpireTime,  // access token 过期时间
        RefreshExpireTime: refreshExpireTime, // refresh token 过期时间
        Issuer:            issuer,            // token 的签发者
    }
}

// GenerateToken 生成 access token 和 refresh token
func (arj *ARJWT) GenerateToken(userId string) (accessToken, refreshToken string, err error) {
   
    // 生成 access token 在 access token 中需要包含我们自定义的字段,比如用户 ID
    mc := JWTCustomClaims{
   
        UserID: userId,
        StandardClaims: jwtPkg.StandardClaims{
   
            // ExpiresAt 是一个时间戳,代表 access token 的过期时间
            ExpiresAt: time.Now().Add(time.Duration(arj.AccessExpireTime) * time.Minute).Unix(),
            // 签发人
            Issuer: arj.Issuer,
        },
    }

    // 生成 access token
    accessToken, err = jwtPkg.NewWithClaims(jwtPkg.SigningMethodHS256, mc).SignedString(arj.Key)
    if err != nil {
   
        log.Printf("generate access token failed: %v \n", err)
        return "", "", err
    }

    // 生成 refresh token
    // refresh token 只需要包含标准的声明,不需要包含自定义的声明
    refreshToken, err = jwtPkg.NewWithClaims(jwtPkg.SigningMethodHS256, jwtPkg.StandardClaims{
   
        // ExpiresAt 是一个时间戳,代表 refresh token 的过期时间
        ExpiresAt: time.Now().Add(time.Duration(arj.RefreshExpireTime) * time.Minute).Unix(),
        // 签发人
        Issuer: arj.Issuer,
    }).SignedString(arj.Key)

    return
}

func (arj *ARJWT) ParseAccessToken(tokenString string) (*JWTCustomClaims, error) {
   
    claims := new(JWTCustomClaims)

    token, err := jwtPkg.ParseWithClaims(tokenString, claims, func(token *jwtPkg.Token) (interface{
   }, error) {
   
        return arj.Key, nil
    })

    if err != nil {
   
        validationErr, ok := err.(*jwtPkg.ValidationError)
        if ok {
   
            switch validationErr.Errors {
   
            case jwtPkg.ValidationErrorMalformed:
                return nil, ErrTokenMalformed
            case jwtPkg.ValidationErrorExpired:
                return nil, ErrTokenExpired
            }
        }
        return nil, ErrTokenInvalid
    }

    if _, ok := token.Claims.(*JWTCustomClaims); ok && token.Valid {
   
        return claims, nil
    }

    return nil, ErrTokenInvalid
}

func (arj *ARJWT) RefreshToken(accessToken, refreshToken string) (newAccessToken, newRefreshToken string, err error) {
   
    // 先判断 refresh token 是否有效
    if _, err = jwtPkg.Parse(refreshToken, func(token *jwtPkg.Token) (interface{
   }, error) {
   
        return arj.Key, nil
    }); err != nil {
   
        return
    }

    // 从旧的 access token 中解析出 JWTCustomClaims 数据出来
    var claims JWTCustomClaims
    _, err = jwtPkg.ParseWithClaims(accessToken, &claims, func(token *jwtPkg.Token) (interface{
   }, error) {
   
        return arj.Key, nil
    })
    if err != nil {
   
        validationErr, ok := err.(*jwtPkg.ValidationError)
        // 当 access token 是过期错误,并且 refresh token 没有过期时就创建一个新的 access token 和 refresh token
        if ok && validationErr.Errors == jwtPkg.ValidationErrorExpired {
   
            // 重新生成新的 access token 和 refresh token
            return arj.GenerateToken(claims.UserID)
        }
    }

    return
}

关于这两种刷新 token 的方式对比,可以直接参考阅读我这里的文章,https://github.com/pudongping/golang-tutorial/tree/main/project/jwt_demo 有比较详细的说明。

结语

通过本文,我们探索了如何在 Go 中使用 Gin 框架实现 JWT 鉴权,包括 token 的生成、解析、刷新等功能。这套方案不仅高效而且易于扩展,可以满足大多数 Web 应用的安全需求。

完整的代码在这里:https://github.com/pudongping/golang-tutorial/blob/main/project/jwt_demo/jwt.go

相关文章
|
9月前
|
存储 前端开发 JavaScript
Cookie、Session、Token、JWT 是什么?万字图解带你一次搞懂!看完这篇,你连老奶奶都能教
HTTP 协议是无状态的,就像一个“健忘”的银行柜员,每次请求都像第一次见面。为解决这一问题,常用的技术包括 Cookie、Session 和 Token。Cookie 是浏览器存储的小数据,Session 将数据存在服务器,Token(如 JWT)则是自包含的无状态令牌,适合分布式和移动端。三者各有优劣,适用于不同场景。
1003 0
Cookie、Session、Token、JWT 是什么?万字图解带你一次搞懂!看完这篇,你连老奶奶都能教
|
10月前
|
存储 Rust 安全
Rocket框架JWT鉴权实战:保护Rust Web API的安全方案​
本篇文章是基于rust语言和rocket依赖实现网页JWT认证和鉴权,完成简单的JWT token的验证和鉴权处理,使用cargo做依赖的导入和测试。
479 0
|
存储 中间件 API
ThinkPHP 集成 jwt 技术 token 验证
本文介绍了在ThinkPHP框架中集成JWT技术进行token验证的流程,包括安装JWT扩展、创建Token服务类、编写中间件进行Token校验、配置路由中间件以及测试Token验证的步骤和代码示例。
ThinkPHP 集成 jwt 技术 token 验证
|
JSON Go API
使用Go语言和Gin框架构建RESTful API:GET与POST请求示例
使用Go语言和Gin框架构建RESTful API:GET与POST请求示例
|
JSON 安全 Java
什么是JWT?如何使用Spring Boot Security实现它?
什么是JWT?如何使用Spring Boot Security实现它?
2439 5
|
SQL Java 测试技术
在Spring boot中 使用JWT和过滤器实现登录认证
在Spring boot中 使用JWT和过滤器实现登录认证
1123 0
|
JSON 安全 Java
使用Spring Boot和JWT实现用户认证
使用Spring Boot和JWT实现用户认证
|
XML JavaScript Java
SpringBoot集成Shiro权限+Jwt认证
本文主要描述如何快速基于SpringBoot 2.5.X版本集成Shiro+JWT框架,让大家快速实现无状态登陆和接口权限认证主体框架,具体业务细节未实现,大家按照实际项目补充。
984 11

热门文章

最新文章