火城二毛 發表於 2023-4-11 14:11:00

Golang 一日一库之jwt-go

<blockquote>
<p>本文地址 https://www.cnblogs.com/zichliang/p/17303759.html</p>
</blockquote>
<p>github地址:https://github.com/dgrijalva/jwt-go</p>
<h1 id="何为-jwt-token">何为 jwt token?</h1>
<p><img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230411141310773-1759403133.png" alt="" loading="lazy"></p>
<p>什么是JSON Web Token?<br>
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON方式安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。<br>
直白的讲jwt就是一种用户认证(区别于session、cookie)的解决方案。</p>
<h1 id="jwt的优势与劣势">jwt的优势与劣势</h1>
<p>优点:</p>
<ol>
<li>多语言支持</li>
<li>通用性好,不存在跨域问题</li>
<li>数据签名相对安全。</li>
<li>不需要服务端集中维护token信息,便于扩展。</li>
</ol>
<p>缺点:<br>
1、用户无法主动登出,只要token在有效期内就有效。这里可以考虑redis设置同token有效期一直的黑名单解决此问题。</p>
<p>2、token过了有效期,无法续签问题。可以考虑通过判断旧的token什么时候到期,过期的时候刷新token续签接口产生新token代替旧token</p>
<h1 id="jwt的构成">JWT的构成</h1>
<h2 id="header">Header</h2>
<p>Header是头部<br>
Jwt的头部承载两部分信息:<br>
声明类型,这里是jwt<br>
声明加密的算法 通常直接使用 HMAC SHA256</p>
<h2 id="playload载荷又称为claim">Playload(载荷又称为Claim)</h2>
<p>playload可以填充两种类型数据<br>
简单来说就是 比如用户名、过期时间等,</p>
<ol>
<li>标准中注册的声明</li>
</ol>
<blockquote>
<p>iss: 签发者<br>
sub: 面向的用户<br>
aud: 接收方<br>
exp: 过期时间<br>
nbf: 生效时间<br>
iat: 签发时间<br>
jti: 唯一身份标识</p>
</blockquote>
<ol start="2">
<li>自定义声明</li>
</ol>
<h2 id="signature签名">Signature(签名)</h2>
<p>是由header、payload 和你自己维护的一个 secret 经过加密得来的<br>
签名的算法:</p>
<pre><code class="language-golang">HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)
</code></pre>
<h1 id="golang-jwtjwt">golang-jwt/jwt</h1>
<h2 id="安装">安装</h2>
<pre><code class="language-bash">go get -u github.com/golang-jwt/jwt/v4
</code></pre>
<p>这里注意 **最新版是V5 但是我们使用的V4, V5 的用法 也一样 不过需要实现Claims的接口方法 一共有六个左右。并且更加严谨了 **</p>
<h2 id="注册声明结构体">注册声明结构体</h2>
<p>注册声明是JWT声明集的结构化版本,仅限于注册声明名称</p>
<pre><code class="language-golang">type JwtCustomClaims struct {
        ID   int
        Name string
        jwt.RegisteredClaims
}
</code></pre>
<h2 id="生成token">生成Token</h2>
<p>首先需要初始化Clamins 其次在初始化结构体中注册并且设置好过期时间 主题 以及生成时间等等。。<br>
然后会发现 jwt.RegisteredClaims<br>
在这个方法中 还需要实现Claims接口 还需要定义几个方法<br>
<img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230411110619949-1099019925.png" alt="" loading="lazy"><br>
如上图所示<br>
然后我们使用<br>
<strong>使用HS256 的签名加密方法使用指定的签名方法和声明创建一个新的</strong><br>
代码如下</p>
<pre><code class="language-golang">// 本文地址 https://www.cnblogs.com/zichliang/p/17303759.html
// GenerateToken 生成Token
func GenerateToken(id int, name string) (string, error) {
        // 初始化
        iJwtCustomClaims := JwtCustomClaims{
                ID:   id,
                Name: name,
                RegisteredClaims: jwt.RegisteredClaims{
                        // 设置过期时间 在当前基础上 添加一个小时后 过期
                        ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("jwt.TokenExpire") * time.Millisecond)),
                        // 颁发时间 也就是生成时间
                        IssuedAt: jwt.NewNumericDate(time.Now()),
                        //主题
                        Subject: "Token",
                },
        }
       
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, iJwtCustomClaims)
        return token.SignedString(stSignKey)
}
</code></pre>
<p>还有一个小坑 这里的<strong>stsignKey</strong> 必须是byte字节的<br>
所以我们在设置签名秘钥 必须要使用byte强转<br>
<img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230411110813330-547148308.png" alt="" loading="lazy"><br>
像这个样子。</p>
<p>然后我们去执行<br>
传入一个ID 和一个name</p>
<pre><code class="language-golang">token, _ := utils.GenerateToken(1, "张三")
fmt.Println(token)
</code></pre>
<p><img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230411111017311-1701337831.png" alt="" loading="lazy"><br>
得到如下值<br>
<code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MSwiTmFtZSI6IuW8oOS4iSIsIlJlZ2lzdGVyZWRDbGFpbXMiOnsic3ViIjoiVG9rZW4iLCJleHAiOjE2ODExODI2MDYsImlhdCI6MTY4MTE4MjYwNn19.AmOf60S2xby6GmlGgNo4Q5b01cRoAqXWhGorzxbJ2-Q</code></p>
<h2 id="解析token">解析Token</h2>
<p>https://jwt.io/<br>
在写代码之前,我们把上面的token丢到上面网站中解析一下<br>
<img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230411111233662-105422129.png" alt="" loading="lazy"><br>
可以发现 有三部分被解析出来了</p>
<ol>
<li>Header 告诉我们用的是什么算法,类型是什么</li>
<li>PayLoad 我们自定义的一些数据</li>
<li>Signature 之后服务器解析做的签名验证</li>
</ol>
<p>代码解析token</p>
<ol>
<li>声明一个空的数据声明</li>
<li>调用 jwt.ParseWithClaims 方法</li>
<li>传入token 数据声明接口,</li>
<li>判断Token是否有效</li>
<li>返回token</li>
</ol>
<pre><code class="language-golang">// ParseToken 解析token
func ParseToken(tokenStr string) (JwtCustomClaims, error) {
        // 声明一个空的数据声明
        iJwtCustomClaims := JwtCustomClaims{}
        //ParseWithClaims是NewParser().ParseWithClaims()的快捷方式
        //第一个值是token ,
        //第二个值是我们之后需要把解析的数据放入的地方,
        //第三个值是Keyfunc将被Parse方法用作回调函数,以提供用于验证的键。函数接收已解析但未验证的令牌。
        token, err := jwt.ParseWithClaims(tokenStr, &amp;iJwtCustomClaims, func(token *jwt.Token) (interface{}, error) {
                return stSignKey, nil
        })

        // 判断 是否为空 或者是否无效只要两边有一处是错误 就返回无效token
        if err != nil &amp;&amp; !token.Valid {
                err = errors.New("invalid Token")
        }
        return iJwtCustomClaims, err
}
</code></pre>
<p>返回成功如下图所示<br>
<img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230411134754364-51703771.png" alt="" loading="lazy"></p>
<p>由于我们主动抛了个错,那我们如果手动传入错的token 看他是否会抛出错误提示呢?</p>
<pre><code class="language-golang">jwtCustomClaim, err := utils.ParseToken(token + "12312323123")
</code></pre>
<p>结果:<br>
<img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230411134915284-1636406132.png" alt="" loading="lazy"></p>
<p>答案是会。</p>
<h2 id="完整代码">完整代码</h2>
<pre><code class="language-golang">package utils

import (
        "errors"
        "fmt"
        "github.com/golang-jwt/jwt/v4"
        "github.com/spf13/viper"
        "time"
)

// 把签发的秘钥 抛出来
var stSignKey = []byte(viper.GetString("jwt.SignKey"))

// JwtCustomClaims 注册声明是JWT声明集的结构化版本,仅限于注册声明名称
type JwtCustomClaims struct {
        ID               int
        Name             string
        RegisteredClaims jwt.RegisteredClaims
}

func (j JwtCustomClaims) Valid() error {
        return nil
}

// GenerateToken 生成Token
func GenerateToken(id int, name string) (string, error) {
        // 初始化
        iJwtCustomClaims := JwtCustomClaims{
                ID:   id,
                Name: name,
                RegisteredClaims: jwt.RegisteredClaims{
                        // 设置过期时间 在当前基础上 添加一个小时后 过期
                        ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("jwt.TokenExpire") * time.Minute)),
                        // 颁发时间 也就是生成时间
                        IssuedAt: jwt.NewNumericDate(time.Now()),
                        //主题
                        Subject: "Token",
                },
        }
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, iJwtCustomClaims)
        return token.SignedString(stSignKey)
}

// ParseToken 解析token
func ParseToken(tokenStr string) (JwtCustomClaims, error) {
        iJwtCustomClaims := JwtCustomClaims{}
        //ParseWithClaims是NewParser().ParseWithClaims()的快捷方式
        token, err := jwt.ParseWithClaims(tokenStr, &amp;iJwtCustomClaims, func(token *jwt.Token) (interface{}, error) {
                return stSignKey, nil
        })

        if err == nil &amp;&amp; !token.Valid {
                err = errors.New("invalid Token")
        }
        return iJwtCustomClaims, err
}

func IsTokenValid(tokenStr string) bool {
        _, err := ParseToken(tokenStr)
        fmt.Println(err)
        if err != nil {
                return false
        }
        return true
}

</code></pre>
<h1 id="dgrijalvajwt-go">dgrijalva/jwt-go</h1>
<h2 id="安装-1">安装</h2>
<pre><code class="language-bash">go get -u "github.com/dgrijalva/jwt-go"
</code></pre>
<h2 id="生成jwt">生成JWT</h2>
<p>这里需要传入用户名和密码<br>
然后根据SHA256 去进行加密 从而吧payload生成token</p>
<pre><code class="language-golang">// 本文地址 https://www.cnblogs.com/zichliang/p/17303759.html
func Macke(user *Userinfo) (token string, err error) {
        claims := jwt.MapClaims{ //创建一个自己的声明
                "name": user.Username,
                "pwd":user.Password,
                "iss":"lva",
                "nbf":time.Now().Unix(),
                "exp":time.Now().Add(time.Second * 4).Unix(),
                "iat":time.Now().Unix(),
        }

        then := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

        token, err = then.SignedString([]byte("gettoken"))

        return
}
</code></pre>
<h2 id="制定解析规则">制定解析规则</h2>
<p><img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230410174340797-807216207.png" alt="" loading="lazy"><br>
在自己写的这个函数中 我们点进源码看返回值<br>
<img src="https://img2023.cnblogs.com/blog/2721529/202304/2721529-20230410174408281-1065385604.png" alt="" loading="lazy"><br>
解析方法使用此回调函数提供用于验证的键。函数接收已解析但未验证的令牌。<br>
这允许您使用令牌Header中的属性(例如' kid ')来识别使用哪个键。</p>
<p>上述是源码的意思 而本人理解是制定一个类型规则然后去做解析。不然源码不知道你是制作token 还是解析token</p>
<pre><code class="language-golang">func secret() jwt.Keyfunc {
        //按照这样的规则解析
        return func(t *jwt.Token) (interface{}, error) {
                return []byte("gettoken"), nil
        }
}
</code></pre>
<h2 id="解析token-1">解析token</h2>
<p>首先需要传入一个token,然后把解析规则传入<br>
然后需要验证Token的正确性以及有效性。<br>
如果二者都是没问题的<br>
然后才能解析出 用户名和密码 或者是其他的一些值</p>
<pre><code class="language-golang">// 解析token
func ParseToken(token string) (user *Userinfo, err error) {
        user = &amp;Userinfo{}
        tokn, _ := jwt.Parse(token, secret())

        claim, ok := tokn.Claims.(jwt.MapClaims)
        if !ok {
                err = errors.New("解析错误")
                return
        }
        if !tokn.Valid {
                err = errors.New("令牌错误!")
                return
        }
        //fmt.Println(claim)
        user.Username = claim["name"].(string) //强行转换为string类型
        user.Password = claim["pwd"].(string)//强行转换为string类型
        return
}
</code></pre>
<h2 id="完整代码-1">完整代码</h2>
<pre><code class="language-golang">// 本文地址 https://www.cnblogs.com/zichliang/p/17303759.html
package main

import (
        "errors"
        "fmt"
        "github.com/dgrijalva/jwt-go"
        "time"
)

type Userinfo struct {
        Username string `json:"username"`
        Password string `json:"password"`
}

// Macke 生成jwt 需要传入 用户名和密码
func Macke(user *Userinfo) (token string, err error) {
        claims := jwt.MapClaims{ //创建一个自己的声明
                "name": user.Username,
                "pwd":user.Password,
                "iss":"lva",
                "nbf":time.Now().Unix(),
                "exp":time.Now().Add(time.Second * 4).Unix(),
                "iat":time.Now().Unix(),
        }

        then := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

        token, err = then.SignedString([]byte("gettoken"))

        return
}

// secret 自己解析的秘钥
func secret() jwt.Keyfunc {
        //按照这样的规则解析
        return func(t *jwt.Token) (interface{}, error) {
                return []byte("gettoken"), nil
        }
}

// 解析token
func ParseToken(token string) (user *Userinfo, err error) {
        user = &amp;Userinfo{}
        tokn, _ := jwt.Parse(token, secret())

        claim, ok := tokn.Claims.(jwt.MapClaims)
        if !ok {
                err = errors.New("解析错误")
                return
        }
        if !tokn.Valid {
                err = errors.New("令牌错误!")
                return
        }
        //fmt.Println(claim)
        user.Username = claim["name"].(string) //强行转换为string类型
        user.Password = claim["pwd"].(string)//强行转换为string类型
        return
}

func main() {
        var use = Userinfo{"zic", "admin*123"}
        tkn, _ := Macke(&amp;use)
        fmt.Println("_____", tkn)
        // time.Sleep(time.Second * 8)超过时间打印令牌错误
        user, err := ParseToken(tkn)
        if err != nil {
                fmt.Println(err)
        }
        fmt.Println(user.Username)
}

</code></pre>
<blockquote>
<p>本文地址 https://www.cnblogs.com/zichliang/p/17303759.html</p>
</blockquote>
<p>这里需要注意<br>
<strong>用户请求时带上token,服务器解析token后可以获得其中的用户信息,如果token有任何改动,都无法通过验证.</strong></p>


</div>
<div id="MySignature" role="contentinfo">
   
<div id="MySignature" style=" color:0; font-size: small"> <p>
      
</div><br><br>
来源:https://www.cnblogs.com/zichliang/p/17303759.html
頁: [1]
查看完整版本: Golang 一日一库之jwt-go