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

相关文章
|
11月前
|
JSON Go API
使用Go语言和Gin框架构建RESTful API:GET与POST请求示例
使用Go语言和Gin框架构建RESTful API:GET与POST请求示例
|
11月前
|
存储 中间件 API
ThinkPHP 集成 jwt 技术 token 验证
本文介绍了在ThinkPHP框架中集成JWT技术进行token验证的流程,包括安装JWT扩展、创建Token服务类、编写中间件进行Token校验、配置路由中间件以及测试Token验证的步骤和代码示例。
ThinkPHP 集成 jwt 技术 token 验证
|
11月前
|
JSON 安全 数据安全/隐私保护
从0到1搭建权限管理系统系列三 .net8 JWT创建Token并使用
【9月更文挑战第22天】在.NET 8中,从零开始搭建权限管理系统并使用JWT(JSON Web Tokens)创建Token是关键步骤。JWT是一种开放标准(RFC 7519),用于安全传输信息,由头部、载荷和签名三部分组成。首先需安装`Microsoft.AspNetCore.Authentication.JwtBearer`包,并在`Program.cs`中配置JWT服务。接着,创建一个静态方法`GenerateToken`生成包含用户名和角色的Token。最后,在控制器中使用`[Authorize]`属性验证和解析Token,从而实现身份验证和授权功能。
881 4
|
12月前
|
API
【Azure Developer】记录一段验证AAD JWT Token时需要设置代理获取openid-configuration内容
【Azure Developer】记录一段验证AAD JWT Token时需要设置代理获取openid-configuration内容
101 0
|
10天前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
13天前
|
数据采集 JSON Go
Go语言实战案例:实现HTTP客户端请求并解析响应
本文是 Go 网络与并发实战系列的第 2 篇,详细介绍如何使用 Go 构建 HTTP 客户端,涵盖请求发送、响应解析、错误处理、Header 与 Body 提取等流程,并通过实战代码演示如何并发请求多个 URL,适合希望掌握 Go 网络编程基础的开发者。
|
14天前
|
JSON 前端开发 Go
Go语言实战:创建一个简单的 HTTP 服务器
本篇是《Go语言101实战》系列之一,讲解如何使用Go构建基础HTTP服务器。涵盖Go语言并发优势、HTTP服务搭建、路由处理、日志记录及测试方法,助你掌握高性能Web服务开发核心技能。
|
19天前
|
Go
如何在Go语言的HTTP请求中设置使用代理服务器
当使用特定的代理时,在某些情况下可能需要认证信息,认证信息可以在代理URL中提供,格式通常是:
99 0
|
6月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
6月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。