欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JWT学习

程序员文章站 2022-06-13 20:07:39
...

基本概念

jwt: JSON Web Token, 原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,
比如生成这样的结构:

{
    "userName": "test",
    "role": "Admin",
    "expire": 1584271948
}

之后,当用户与服务器通信时,客户在每次请求时都要带上这个JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名(下面会说)。

这样的话, 服务器就不用保存会话数据了,即服务器变为无状态,使其更容易扩展。

当然为了保密, 不会明文传输这个JSON, 而是将其打包到token中, 用户其实每次还给服务端的是这个token.
token是什么样子呢?
三部分: X.Y.Z (其中X, Y, Z分别代表三个字符串), 比如我临时生成的一个:
其中:
X: JWT头(Base64URL编码)
Y: 实际数据(Base64URL编码)
Z: 签名

比如对于下面的token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJtYWlsLnRlc3QuY29tIiwiZXhwIjoxNTg0MjcxODEwLCJpYXQiOjE1ODQyNzE3NTAsImlzcyI6InBzc3BvcnQudGVzdC5jb20iLCJuYmYiOjE1ODQyNzE3NTAsInN1YiI6IjEyMzQ1QHRlc3QuY29tIn0.8XbiPJ212JWUwUBmhtrBWbmVdB-jQdayyO29cAUd5qk

JWT头

$ echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d | jq
{
  "alg": "HS256",
  "typ": "JWT"
}

解析头部, 可以得到上述结果. alg表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ表示token的类型,JWT令牌统一写为JWT。

数据部分

$ echo 'eyJhdWQiOiJtYWlsLnRlc3QuY29tIiwiZXhwIjoxNTg0MjcxODEwLCJpYXQiOjE1ODQyNzE3NTAsImlzcyI6InBzc3BvcnQudGVzdC5jb20iLCJuYmYiOjE1ODQyNzE3NTAsInN1YiI6IjEyMzQ1QHRlc3QuY29tIn0' | base64 -d | jq
base64: invalid input
{
  "aud": "mail.test.com",
  "exp": 1584271810,
  "iat": 1584271750,
  "iss": "pssport.test.com",
  "nbf": 1584271750,
  "sub": "aaa@qq.com"
}

数据部分可以任意存放自己需要的值, 可以看到也是base64编码的, 所以像一些敏感信息就不能放在这里了, 上述只是举例.
注: 上面的"base64: invalid input"是因为最后少一个=,
用linux的base64命令解析会有警告, 不影响.

签名

签名部分就是个普通的字符串, 不是base64编码的.
计算方法(以HMACSHA256为例子):
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),
secret)

JWT头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见Base64算法类似,稍有差别。

作为令牌的JWT可以放在URL中(例如test.com/?token=xxx)。 Base64中用的三个字符是+/=,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:=去掉,+-替换,/_替换.

我们可以来验证一下, 上面的token用的**是"s3cret".
JWT学习
https://1024tools.com/hmac这个页面中, 输入图中参数,
其中消息是"X.Y",
看最后一行的结果"结果B:(HMAC计算返回原始二进制数据后进行Base64编码)":

8XbiPJ212JWUwUBmhtrBWbmVdB+jQdayyO29cAUd5qk=

按Base64URL的规则, 将计算结果的+改为-, =去掉, 就和上述的Z部分一样了.

实践

package main

import (
    "fmt"
    "time"

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

var (
    secret = []byte("s3cret")
)

func main() {
    token, err := CreateToken(secret)
    if err != nil {
        panic(err)
    }
    fmt.Println(token)

    payload, err := ParseToken(token)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", payload)
}

// Ref: https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac
func CreateToken(secret []byte) (token string, err error) {
    // 创建token对象, 指定了签名算法和请求参数
    jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
        Audience:  "mail.test.com",        // 接收方
        ExpiresAt: time.Now().Unix() + 60, // 过期时间
        IssuedAt:  time.Now().Unix(),      // 签发时间
        Issuer:    "pssport.test.com",     // 签发者
        NotBefore: time.Now().Unix(),      // 在此时间之前不可用
        Subject:   "aaa@qq.com",       // 面向的用户
    })

    // 签名 并 得到 使用**签名后的token
    if token, err = jwtToken.SignedString(secret); err != nil {
        return
    }
    return
}

// Ref: https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac
func ParseToken(tokenString string) (payload map[string]interface{}, err error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Don't forget to validate the alg is what you expect:
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return secret, nil
    })

    if err != nil {
        return
    }

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        payload = claims
    } else {
        err = fmt.Errorf("unexpected claims")
    }
    return
}

上面的例子演示了如何创建token, 及验证token, 解析出实际数据.

JWT的一般用法

客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header字段中。

参考

相关标签: JWT