介绍

全称 JSON Web Token,分为 HeaderPayloadSignature 三个部分,应用中,服务端生成一个 JSON 格式的对象,经过加密后生成 Header.Payload.Signature 这样以点分割的字符串,返回给客户端,之后客户端使用这个字符串作为身份凭证与服务器交互。

JWT 可以分布式生成,将用户非核心的信息存入 JWT 中,使服务端不需要单独的维护用户登录凭证,同时不是将用户的凭证存入 Cookie 中,可以有效的防止 CRSF 攻击。

组成

上面说了 JWT 由三部分组成,它们默认是不加密的,使用 Base64URL 算法转为字符串,然后以点分割组成,下面对其一一学习和介绍。

  • Header 头部
  • Payload 载荷
  • Signature 签名

image-20230804192513606

Base64URLBase64 的区别

Base64URL使用的字符集与Base64相同,但是将"+"替换为"-",将"/"替换为"_",并且不包含Base64中的"="用于填充。

例如,一个字符串"Hello, World!"的Base64编码结果是"SGVsbG8sIFdvcmxkIQ==",而相同字符串的Base64URL编码结果是"SGVsbG8sIFdvcmxkIQ"。

Header

头部用于存储 JWT 的元信息,说明使用的类型和签名算法,是一个 JSON 格式对象

头部一般格式如下:

{
  "alg": "HS256",
  "typ": "JWT"
}

typ 统一为 JWTalg 是签名算法

function base64UrlEncode(str) {
  let base64 = window.btoa(str);
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
var header=base64UrlEncode(JSON.stringify({
  "alg": "HS256",
  "typ": "JWT"
}));
console.log(header);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 就是服务器返回给客户端的 Header 部分。仔细去观察,很多网站都是这个前缀。

Payload

载荷用于存储我们所需要存放的数据,JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

也是使用一个 JSON 对象来存储数据,因为默认是不加密的,所以千万不要把用户的密码存储到这里。

function base64UrlEncode(str) {
  let base64 = window.btoa(str);
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
var data={
  "user_id": 12345678,
  "username": "xxcheng",
  "exp": 1791145041,
  "iss": "xxcheng"
}
var payload=base64UrlEncode(JSON.stringify(data))
console.log(payload);
// eyJ1c2VyX2lkIjoxMjM0NTY3OCwidXNlcm5hbWUiOiJ4eGNoZW5nIiwiZXhwIjoxNzkxMTQ1MDQxLCJpc3MiOiJ4eGNoZW5nIn0

eyJ1c2VyX2lkIjoxMjM0NTY3OCwidXNlcm5hbWUiOiJ4eGNoZW5nIiwiZXhwIjoxNzkxMTQ1MDQxLCJpc3MiOiJ4eGNoZW5nIn0 就是载荷部分,通过简单的 base64 转换就可以获取原来数据,千万不能存隐私数据。

Signature

签名是对签名两个部分的签名进行签名防止被篡改,需要提供一个密钥用于加盐,这个密钥必须保存好,不能泄露。

function base64UrlEncode(str) {
  let base64 = window.btoa(str);
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
var secret="xxcheng"
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
// qQBFCMuZLeQoirXhJ-VVOKcnEgDqXcU5QytR8oYXWRI

分类

Token 可以分为两种:Access TokenRefresh Token,利用 Refresh Token,在 Access Token 要过期后,重新生成一个新的 Access Token 返回给前端。Access Token 的有效期较短,Refresh Token 用于签发新的 Access Token,有效期较长。这样子可以防止 Access Token 泄露,而无法废止的弊端(或者废止成本高,又回到了之前需要集中统一管理的情况,Refresh Token 虽然可能需要建个授权服务器,但是它不服务那些高负载的校验请求,只服务更新 Token 这一服务)。

Go 实现

使用 github.com/dgrijalva/jwt-go 这个库实现

Payload

先定义一个用于存储数据的载荷结构体,同时 jwt-go 为我们一个官方的七种的载荷的结构体 StandardClaims

type MyClaims struct {
    UserID   int64  `json:"user_id"`
    Username string `json:"username"`
    jwt.StandardClaims
}

信息配置

// 有效期
const JWTTimeExpireDuration = time.Minute * 1

// 加盐密钥
var secret = []byte("ZyRyJoZzz2ygTE1M8qOW0XFGcSulFRo40Fk871lYQp810RTW")

// 签发人
const Issuer = "xxcheng"

生成

func Test_GenerateToken(t *testing.T) {
    mc := &MyClaims{
        UserID:   12345678,
        Username: "jpc",
        StandardClaims: jwt.StandardClaims{
            //使用UUID生成的一个,用于保证唯一
            Id: "d1414649-5a85-4a10-b1c5-befc1ac005d3",
            //设置过期时间
            ExpiresAt: time.Now().Add(JWTTimeExpireDuration).Unix(),
            //签发人
            Issuer: Issuer,
        },
    }
    //选择头部信息和载荷
    //此时是未加密的
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, mc)
    //打印头部
    fmt.Printf("%T,%#v\n", token.Header, token.Header)
    //打印载荷
    fmt.Printf("%T,%#v\n", token.Claims, token.Claims)
    tokenStr, _ := token.SignedString(secret)
    fmt.Println("------")
    fmt.Println(tokenStr)
}
map[string]interface {},map[string]interface {}{"alg":"HS256", "typ":"JWT"}
*jwt.MyClaims,&jwt.MyClaims{UserID:12345678, Username:"jpc", StandardClaims:jwt.StandardClaims{Audience:"", ExpiresAt:1691150388, Id:"d1414649-5a85-4a10-b1c5-befc1
ac005d3", IssuedAt:0, Issuer:"xxcheng", NotBefore:0, Subject:""}}
------
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3OCwidXNlcm5hbWUiOiJqcGMiLCJleHAiOjE2OTExNTAzODgsImp0aSI6ImQxNDE0NjQ5LTVhODUtNGExMC1iMWM1LWJlZmMxYWMwMDVkMyIsImlzcyI6Inh4Y2hlbmcifQ.dtSHOqI9ovFFQMJ-2y4_ybrwZTXMy31zzxfun4SluTc

解析

func Test_ParseToken(t *testing.T) {
    mc := new(MyClaims)
    tokenStr := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3OCwidXNlcm5hbWUiOiJqcGMiLCJleHAiOjE2OTExNTAzODgsImp0aSI6ImQxNDE0NjQ5LTVhODUtNGExMC1iMWM1LWJlZmMxYWMwMDVkMyIsImlzcyI6Inh4Y2hlbmcifQ.dtSHOqI9ovFFQMJ-2y4_ybrwZTXMy31zzxfun4SluTc"
    _, err := jwt.ParseWithClaims(tokenStr, mc, func(token *jwt.Token) (interface{}, error) {
        return secret, nil
    })
    if err != nil {
        v, _ := err.(*jwt.ValidationError)
        switch v.Errors {
        case jwt.ValidationErrorExpired:
            fmt.Println("过期~~~")

        }
        return
    }
    fmt.Println(mc)
}
过期~~~

上面的 Token 过期了,生成一个新的重新测试

&{12345678 jpc { 1691150871 d1414649-5a85-4a10-b1c5-befc1ac005d3 0 xxcheng 0 }}

参考链接

文章目录