使用 Base64 解码 JWT Playload 数据不完整

2023年 1月 4日 104.3k 0

在对 JWT 进行 Base64 解码时,发现 JSON 数据不完整。本文主要介绍相关知识点并解决这个问题。

1. JWT 简介

JWT 通过在 Header 中设置 Authorization: Bearer <token> 进行认证的传递。JWT Token 是一个 . 连接的 Base64 编码字符串,类似这样 Header.Payload.Signature ,有三部分组成:

  • Header ,定义 Token 类型和加密算法
1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Payload ,负载信息,通常是 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)等
1
2
3
4
5
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
  • Signature ,对 Base64 编码的 Header 和 Playload 进行签名,防止信息被篡改。
1
2
3
4
5
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

jwt.io 提供了一个在线解析 Token 的工具。

2. Base64 解码

“encoding/base64” 提供了四种编码和解码的方法:

  • StdEncoding , 常规编码,不足 3 倍时,使用 = 补齐
  • URLEncoding , URL safe 编码,替换掉字符串中的特殊字符 +/ 转化成 -_
  • RawStdEncoding , 常规编码,末尾不补 =
  • RawURLEncoding , URL safe 编码,末尾不补 =

下面,通过具体代码,看看它们之间的差别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
    "encoding/base64"
    "fmt"
)

func coding(msg []byte){
    fmt.Println("Input :", string(msg))

    encoded := base64.StdEncoding.EncodeToString(msg)
    fmt.Println("StdEncoding :", encoded)
    decoded, _ := base64.StdEncoding.DecodeString(encoded)
    fmt.Println("StdEncoding :", string(decoded))

    encoded = base64.URLEncoding.EncodeToString(msg)
    fmt.Println("URLEncoding :", encoded)
    decoded, _ = base64.URLEncoding.DecodeString(encoded)
    fmt.Println("URLEncoding :", string(decoded))

    encoded = base64.RawStdEncoding.EncodeToString(msg)
    fmt.Println("RawStdEncoding :", encoded)
    decoded, _ = base64.RawStdEncoding.DecodeString(encoded)
    fmt.Println("RawStdEncoding :", string(decoded))

    encoded = base64.RawURLEncoding.EncodeToString(msg)
    fmt.Println("RawURLEncoding :", encoded)
    decoded, _ = base64.RawURLEncoding.DecodeString(encoded)
    fmt.Println("RawURLEncoding :", string(decoded))
}

func main() {
    // 补齐
    coding([]byte("https://www.chenshaowen.com/"))
    // URL Safe 编码
    coding([]byte("abc123!?$*&()'[email protected]~"))
    
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Input : https://www.chenshaowen.com/
StdEncoding : aHR0cHM6Ly93d3cuY2hlbnNoYW93ZW4uY29tLw==
StdEncoding : https://www.chenshaowen.com/
URLEncoding : aHR0cHM6Ly93d3cuY2hlbnNoYW93ZW4uY29tLw==
URLEncoding : https://www.chenshaowen.com/
RawStdEncoding : aHR0cHM6Ly93d3cuY2hlbnNoYW93ZW4uY29tLw
RawStdEncoding : https://www.chenshaowen.com/
RawURLEncoding : aHR0cHM6Ly93d3cuY2hlbnNoYW93ZW4uY29tLw
RawURLEncoding : https://www.chenshaowen.com/
Input : abc123!?$*&()'[email protected]~
StdEncoding : YWJjMTIzIT8kKiYoKSctPUB+
StdEncoding : abc123!?$*&()'[email protected]~
URLEncoding : YWJjMTIzIT8kKiYoKSctPUB-
URLEncoding : abc123!?$*&()'[email protected]~
RawStdEncoding : YWJjMTIzIT8kKiYoKSctPUB+
RawStdEncoding : abc123!?$*&()'[email protected]~
RawURLEncoding : YWJjMTIzIT8kKiYoKSctPUB-
RawURLEncoding : abc123!?$*&()'[email protected]~

从输出的结果来看:

  • Stdxxx 会对 Base64 编码执行补齐
  • URLxxx 会对 Base64 编码进行转码
  • Base64 是公开的标准编码规则,但不同的库实现时,暴露出来的接口会有差异,使用正确的接口才能获得预期的结果。

    3. JWT Playload 少了一部分

    下面这段代码截取了 Playload 部分进行解析:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    package main
    
    import (
        "encoding/base64"
        "fmt"
    )
    
    func main() {
        decoded := "eyJ1c2VybmFtZSI6ImFkbWluIiwidWlkIjoiYjhiZTZlZGQtMmM5Mi00NTM1LTliMmEtZGY2MzI2NDc0NDU4IiwiaWF0IjoxNTkxMzU0MDEwLCJpc3MiOiJrdWJlc3BoZXJlIiwibmJmIjoxNTkxMzU0MDEwfQ"
        encoded, _ := base64.StdEncoding.DecodeString(decoded)
        fmt.Println(string(encoded))
    }
    

    得到结果:

    1
    
    {"username":"admin","uid":"b8be6edd-2c92-4535-9b2a-df6326474458","iat":1591354010,"iss":"","nbf":1591354010
    

    发现,这并不是一个完整的 Json 对象。在 dgrijalva/jwt-go 库中,可以看到 EncodeSegment 函数的实现:

    1
    2
    3
    4
    
    // Encode JWT specific base64url encoding with padding stripped
    func EncodeSegment(seg []byte) string {
    	return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
    }
    

    显然,dgrijalva/jwt-go 使用的是 RawURLEncoding 的方式进行编码。调整之后,执行下面这段代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    package main
    
    import (
        "encoding/base64"
        "fmt"
    )
    
    func main() {
        decoded := "eyJ1c2VybmFtZSI6ImFkbWluIiwidWlkIjoiYjhiZTZlZGQtMmM5Mi00NTM1LTliMmEtZGY2MzI2NDc0NDU4IiwiaWF0IjoxNTkxMzU0MDEwLCJpc3MiOiJrdWJlc3BoZXJlIiwibmJmIjoxNTkxMzU0MDEwfQ"
        encoded, _ := base64.RawURLEncoding.DecodeString(decoded)
        fmt.Println(string(encoded))
    }
    

    得到正确结果:

    1
    
    {"username":"admin","uid":"b8be6edd-2c92-4535-9b2a-df6326474458","iat":1591354010,"iss":"","nbf":1591354010}
    

    另外一种方式是,使用 dgrijalva/jwt-go 内置的解析器,提供完整的 JWT Token 进行解析。可以看看下面这段代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package main
    
    import (
        "github.com/dgrijalva/jwt-go"
        "fmt"
    )
    
    func main() {
        decoded := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwidWlkIjoiYjhiZTZlZGQtMmM5Mi00NTM1LTliMmEtZGY2MzI2NDc0NDU4IiwiaWF0IjoxNTkxMzU0MDEwLCJpc3MiOiJrdWJlc3BoZXJlIiwibmJmIjoxNTkxMzU0MDEwfQ.psKkj8vYWm9Crf9jnbB_PNestLNksaS9vuMvQI3C-dU"
        type Claims struct {
            Username string `json:"username"`
            UID      string `json:"uid"`
            jwt.StandardClaims
        }
    
        claim := Claims{}
        parser := jwt.Parser{}
        parser.ParseUnverified(decoded, &claim)
        fmt.Println(claim.Username)
    }
    

    得到预期结果:

    admin
    

    4. 参考

    • https://jwt.io/

    相关文章

    KubeSphere 部署向量数据库 Milvus 实战指南
    探索 Kubernetes 持久化存储之 Longhorn 初窥门径
    征服 Docker 镜像访问限制!KubeSphere v3.4.1 成功部署全攻略
    那些年在 Terraform 上吃到的糖和踩过的坑
    无需 Kubernetes 测试 Kubernetes 网络实现
    Kubernetes v1.31 中的移除和主要变更

    发布评论