高峡平湖 發表於 2025-11-21 10:09:57

Go语言jwt跨域鉴权的实现实例

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">jwt介绍</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">JWT 到底是什么?</a></li><li><a href="#_lab2_0_1">JWT 和 OAuth</a></li><li><a href="#_lab2_0_2">选择签名方法</a></li><li><a href="#_lab2_0_3">签名方法和密钥类型</a></li></ul><li><a href="#_label1">安装jwt</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_4">简单使用</a></li><ul class="third_class_ul"><li><a href="#_label3_1_4_0">生成JWT</a></li><li><a href="#_label3_1_4_1">解析jwt</a></li><li><a href="#_label3_1_4_2">测试:生成token并解析token</a></li></ul><li><a href="#_lab2_1_5">jwt在项目中的使用</a></li><ul class="third_class_ul"><li><a href="#_label3_1_5_3">第一步:在一个go文件中,写生成jwt和解析jwt的方法,方便调用</a></li><li><a href="#_label3_1_5_4">第二步:登陆的时候生成token</a></li><li><a href="#_label3_1_5_5">第三步:在控制器中写一个go文件,JWTAuthMiddleware基于JWT的认证中间件</a></li><li><a href="#_label3_1_5_6">第四部:登陆验证token</a></li></ul></ul></ul></div><p>JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token实现方式,目前多用于前后端分离项目和OAuth2.0业务场景下。</p>
<p class="maodian"><a name="_label0"></a></p><h2>jwt介绍</h2>
<blockquote><p>官网:<a href="https://jwt.io/" rel="external nofollow"target="_blank">https://jwt.io/</a></p></blockquote>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>JWT 到底是什么?</h3>
<p>简而言之,jwt是一个签名的 JSON 对象,可以做一些有用的事情(例如,身份验证)。它通常用于BearerOauth 2 中的令牌。令牌由三部分组成,由.&#39;s 分隔。前两部分是 JSON 对象,已经过base64url编码。最后一部分是签名,以同样的方式编码。</p>
<p>第一部分称为标题。它包含验证最后一部分签名的必要信息。例如,使用哪种加密方法进行签名以及使用了什么密钥。</p>
<p>中间的部分是有趣的部分。它称为声明,包含您关心的实际内容。有关保留密钥和添加自己的正确方法的信息。</p>
<p class="maodian"><a name="_lab2_0_1"></a></p><h3>JWT 和 OAuth</h3>
<p>值得一提的是,OAuth 和 JWT 不是一回事。JWT 令牌只是一个签名的 JSON 对象。它可以在任何有用的地方使用。但是,存在一些混淆,因为 JWT 是 OAuth2 身份验证中最常见的不记名令牌类型。</p>
<p>不用太深入,这里是对这些技术交互的描述:</p>
<ul><li>OAuth 是一种允许身份提供者与用户登录的服务分开的协议。例如,每当您使用 Facebook 登录不同的服务(Yelp、Spotify 等)时,您都在使用 OAuth。</li><li>OAuth 定义了几个用于传递身份验证数据的选项。一种流行的方法称为&ldquo;不记名令牌&rdquo;。不记名令牌只是一个字符串,只能由经过身份验证的用户持有。因此,只需出示此令牌即可证明您的身份。您可能可以从这里得出为什么 JWT 可能会成为一个好的不记名令牌。</li><li>因为不记名令牌用于身份验证,所以对它们保密很重要。这就是使用不记名令牌的交易通常通过 SSL 发生的原因。</li></ul>
<p class="maodian"><a name="_lab2_0_2"></a></p><h3>选择签名方法</h3>
<p>有几种可用的签名方法,您可能应该花时间了解各种选项,然后再选择一种。主要的设计决策很可能是对称的还是非对称的。</p>
<p><strong>对称签名方法</strong>(例如 HSA)仅使用一个密钥。这可能是最简单的签名方法,因为任何<code>[]byte</code>都可以用作有效的秘密。它们在计算上的使用速度也略快一些,尽管这很少有关系。当令牌的生产者和消费者都受信任,甚至是同一个系统时,对称签名方法效果最好。由于相同的密钥用于签名和验证令牌,因此您无法轻松分发密钥以进行验证。</p>
<p><strong>非对称签名方法</strong>(例如 RSA)使用不同的密钥来签名和验证令牌。这使得使用私钥生成令牌成为可能,并允许任何消费者访问公钥进行验证。</p>
<p class="maodian"><a name="_lab2_0_3"></a></p><h3>签名方法和密钥类型</h3>
<p><code>jwt-go</code>库支持 JWT 的解析和验证以及生成和签名。当前支持的签名算法是 HMAC SHA、RSA、RSA-PSS 和 ECDSA</p>
<p>每个签名方法都需要不同的对象类型作为其签名密钥。有关详细信息,请参阅软件包文档。以下是最常见的:</p>
<ul><li><a href="https://godoc.org/github.com/dgrijalva/jwt-go" rel="external nofollow"   target="_blank">HMAC 签名方法</a>( <code>HS256</code>, <code>HS384</code>, <code>HS512</code>)<code>[]byte</code>需要用于签名和验证的值</li><li><a href="https://godoc.org/github.com/dgrijalva/jwt-go" rel="external nofollow">RSA 签名方法</a>( , <code>RS256</code>, <code>RS384</code>)<code>RS512</code>期望<code>*rsa.PrivateKey</code>用于签名和<code>*rsa.PublicKey</code>验证</li><li><a href="https://godoc.org/github.com/dgrijalva/jwt-go" rel="external nofollow">ECDSA 签名方法</a>( , <code>ES256</code>, <code>ES384</code>)<code>ES512</code>期望<code>*ecdsa.PrivateKey</code>用于签名和<code>*ecdsa.PublicKey</code>验证</li></ul>
<p class="maodian"><a name="_label1"></a></p><h2>安装jwt</h2>
<div class="jb51code"><pre class="brush:go;">go get github.com/dgrijalva/jwt-go
</pre></div>
<p class="maodian"><a name="_lab2_1_4"></a></p><h3>简单使用</h3>
<p class="maodian"><a name="_label3_1_4_0"></a></p><h4>生成JWT</h4>
<div class="jb51code"><pre class="brush:go;">type MyClaims struct {
        //除了满足下面的Claims,还需要以下用户信息
        Username string `json:"username"`
        Password string `json:"password"`
        //jwt中标准的Claims
        jwt.StandardClaims
}

// 使用指定的 secret 签名声明一个 key ,便于后续获得完整的编码后的字符串token
var key = []byte("secret")

//GenToken 生成token的方法
func GenToken(username string, password string) (string, error) {
        //创建一个我们自己的声明
        c := MyClaims{
                username, //自定义字段
                password,
                jwt.StandardClaims{
                        ExpiresAt: time.Now().Add(time.Hour * 2).Unix(), //过期时间
                        Issuer:    "Psych",                              //签发人
                },
        }

        //使用指定的签名方法创建签名对象
        //这里使用HS256加密算法
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)

        //注意这个地方的 key 一定要是字节切片不能是字符串
        return token.SignedString(key)
}
</pre></div>
<p class="maodian"><a name="_label3_1_4_1"></a></p><h4>解析jwt</h4>
<div class="jb51code"><pre class="brush:go;">//ParseToken 解析token的方法
func ParseToken(tokenString string) (*MyClaims, error) {
        //解析token
        token, err := jwt.ParseWithClaims(tokenString, &amp;MyClaims{},
                func(token *jwt.Token) (i interface{}, err error) {
                        return key, nil
                })
        if err != nil {
                return nil, err
        }

        if claims, ok := token.Claims.(*MyClaims); ok &amp;&amp; token.Valid { //校验token
                return claims, nil
        }
        return nil, errors.New("invalid token")
}
</pre></div>
<p class="maodian"><a name="_label3_1_4_2"></a></p><h4>测试:生成token并解析token</h4>
<div class="jb51code"><pre class="brush:go;">func main() {
        //生成token
        token, err := GenToken("Psych", "123456")
        if err != nil {
                panic(err)
        }
        fmt.Printf("token: %v\n", token)

        fmt.Println("----------------------")

        //解析token
        parseToken, err := ParseToken(token)
        if err != nil {
                panic(err)
        }
        fmt.Printf("parseToken.UserName: %v\n", parseToken.Username)
        fmt.Printf("parseToken.Password: %v\n", parseToken.Password)
}
</pre></div>
<p>运行结果:</p>
<div class="jb51code"><pre class="brush:plain;"> go run "e:\golang开发学习\go_pro\main.go"
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlBzeWNoIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJleHAiOjE2NjI1MzQ5MTEsImlzcyI6IlBzeWNoIn0.hsqMq08NB2uz5OSMXB5KT-77WlirvcSX8kgtyWIVEP0
----------------------
parseToken.UserName: Psych
parseToken.Password: 123456

exited with code=0 in 1.572 seconds
</pre></div>
<p class="maodian"><a name="_lab2_1_5"></a></p><h3>jwt在项目中的使用</h3>
<p class="maodian"><a name="_label3_1_5_3"></a></p><h4>第一步:在一个go文件中,写生成jwt和解析jwt的方法,方便调用</h4>
<div class="jb51code"><pre class="brush:go;">package jwt

import (
        "errors"
        "time"

        "github.com/dgrijalva/jwt-go"
)

//载荷
type Customclaims struct {
        Empname string `json:"empname"`
        Phone   string `json:"phone"`
        Role    string `json:"role"`
        jwt.StandardClaims
}

// MyClaims 自定义声明结构体并内嵌jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们这里需要额外记录一个UserID字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyClaims struct {
        UserName string `json:"user_name"`
        jwt.StandardClaims
}

var mySecret = []byte("呼哧呼哧")

func keyFunc(_ *jwt.Token) (i interface{}, err error) {
        return mySecret, nil
}

const TokenExpireDuration = time.Hour * 24 * 365

// GenToken 生成access token 和 refresh token
func GenToken(userName string) (aToken, rToken string, err error) {
        // 创建一个我们自己的声明
        c := MyClaims{
                userName, // 自定义字段
                jwt.StandardClaims{
                        ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
                        Issuer:    "personal-blog",                            // 签发人
                },
        }
        // 加密并获得完整的编码后的字符串token
        aToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, c).SignedString(mySecret)

        // refresh token 不需要存任何自定义数据
        rToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
                ExpiresAt: time.Now().Add(time.Second * 30).Unix(), // 过期时间
                Issuer:    "personal-blog",                         // 签发人
        }).SignedString(mySecret)
        // 使用指定的secret签名并获得完整的编码后的字符串token
        return
}

// ParseToken 解析JWT
func ParseToken(tokenString string) (claims *MyClaims, err error) {
        // 解析token
        var token *jwt.Token
        claims = new(MyClaims)
        token, err = jwt.ParseWithClaims(tokenString, claims, keyFunc)
        if err != nil {
                return
        }
        if !token.Valid { // 校验token
                err = errors.New("invalid token")
        }
        return
}

// RefreshToken 刷新AccessToken
func RefreshToken(aToken, rToken string) (newAToken, newRToken string, err error) {

        // 从旧access token中解析出claims数据        解析出payload负载信息
        var claims MyClaims
        _, err = jwt.ParseWithClaims(aToken, &amp;claims, keyFunc)

        // 当access token是过期错误 并且 refresh token没有过期时就创建一个新的access token

        return GenToken(claims.UserName)

}
</pre></div>
<p class="maodian"><a name="_label3_1_5_4"></a></p><h4>第二步:登陆的时候生成token</h4>
<div class="jb51code"><pre class="brush:go;">//登录
func LoginHandler(c *gin.Context) {
        // 1.获取请求参数 2.校验数据有效性
        var L models.Users
        if err := c.ShouldBindJSON(&amp;L); err != nil {
                zap.L().Error("invalid params", zap.Error(err))
                ResponseErrorWithMsg(c, CodeInvalidParams, err.Error())
                return
        }
        var my models.Update_my
        //用户登录
        if err, a := mysql.Login(&amp;L); err != nil {
                zap.L().Error("mysql.Login(&amp;u) failed", zap.Error(err))
                ResponseError(c, CodeInvalidPassword)
                return
        } else {
                my = a
        }

        // 生成Token
        aToken, rToken, _ := jwt.GenToken(L.UserName)
        ResponseSuccess(c, gin.H{
                "accessToken":aToken,
                "refreshToken": rToken,
                "username":   L.UserName,
                "role":         L.Role,
                "realname":   my.Realname,
                "phone_number": my.PhoneNumber,
                "id_number":    my.IDNumber,
        })
}
</pre></div>
<p class="maodian"><a name="_label3_1_5_5"></a></p><h4>第三步:在控制器中写一个go文件,JWTAuthMiddleware基于JWT的认证中间件</h4>
<div class="jb51code"><pre class="brush:go;">package controller

import (
        "Fever_backend/dao/mysql"
        "Fever_backend/pkg/jwt"
        "errors"
        "fmt"
        "go.uber.org/zap"
        "net/http"
        "strings"

        "github.com/gin-gonic/gin"
)

const (
        ContextUserNameKey = "userName"
)

var (
        ErrorUserNotLogin = errors.New("当前用户未登录")
)

// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
        return func(c *gin.Context) {
                // 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI
                // 这里假设Token放在Header的Authorization中,并使用Bearer开头
                // 这里的具体实现方式要依据你的实际业务情况决定
                authHeader := c.Request.Header.Get("Authorization")
                if authHeader == "" {
                        ResponseErrorWithMsg(c, CodeInvalidToken, "请求头缺少Auth Token")
                        c.Abort()
                        return
                }
                // 按空格分割
                parts := strings.SplitN(authHeader, " ", 2)
                if !(len(parts) == 2 &amp;&amp; parts == "Bearer") {
                        ResponseErrorWithMsg(c, CodeInvalidToken, "Token格式不对")
                        c.Abort()
                        return
                }
                // parts是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
                mc, err := jwt.ParseToken(parts)
                if err != nil {
                        fmt.Println(err)
                        ResponseError(c, CodeInvalidToken)
                        c.Abort()
                        return
                }
                // 将当前请求的username信息保存到请求的上下文c上
                c.Set(ContextUserNameKey, mc.UserName)
                c.Next() // 后续的处理函数可以用过c.Get("userID")来获取当前请求的用户信息
        }
}
</pre></div>
<p class="maodian"><a name="_label3_1_5_6"></a></p><h4>第四部:登陆验证token</h4>
<div class="jb51code"><pre class="brush:go;">//这里只是举了个例,具体业务需要具体分析
//登录验证token
        v1.Use(controller.JWTAuthMiddleware())
        {
                //修改密码
                v1.POST("/change_password", controller.ChangePasswordHandler)
                //加权限
                v1.POST("/add_casbin", controller.AddCasbin)
        }

</pre></div>
頁: [1]
查看完整版本: Go语言jwt跨域鉴权的实现实例