RSA与JWT避坑
Published in:2026-01-20 |
Words: 1.2k | Reading time: 5min | reading:

一、 背景与挑战

将 Java SSO SDK 迁移至 Python 云函数(Serverless)时,我们面临三大挑战:

  1. 环境限制:云函数无法安装 rsapyjwt 等第三方库,必须使用原生代码。
  2. 非标协议:JWT 签名逻辑与标准规范不一致,直接导致服务端报 Object reference 空指针错误。
  3. 参数陷阱:跳转参数名特定为 return_url,且对 URL 编码有严格要求。

二、 核心算法流程图解

1. 非标准 JWT 生成流程(关键差异)

标准 JWT 是对 “UrlSafeBase64” 字符串进行签名,而 对 “StandardBase64” 字符串进行签名。这是导致验签失败的根本原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
graph LR
subgraph 数据准备
H[Header JSON]
P[Payload JSON]
end

subgraph 步骤1_非标编码
H -->|转标准Base64| H_Std[Std_B64_Header]
P -->|转标准Base64| P_Std[Std_B64_Payload]
H_Std & P_Std -->|拼接| Content["H_Std + '.' + P_Std"]
end

subgraph 步骤2_RSA签名
Content -->|SHA256 + PKCS#1| Sign[[RSA签名]]
Sign --> Sig_Std[Std_B64_Signature]
end

subgraph 步骤3_最终输出
H_Std -->|转URLSafe| H_Safe
P_Std -->|转URLSafe| P_Safe
Sig_Std -->|转URLSafe| Sig_Safe

H_Safe & P_Safe & Sig_Safe -->|拼接| Token["最终 ID_TOKEN"]
end

2. 纯 Python 实现 RSA 签名(零依赖)

为了摆脱对第三方库的依赖,我们利用 Python 内置的大数运算 pow(m, d, n) 手动实现了 PKCS#1 v1.5 签名。

1
2
3
4
5
6
7
8
9
10
11
12
graph TD
Key[PEM格式私钥] -->|Base64解码 + ASN.1解析| Data(提取模数 n, 指数 d)

Msg[待签名内容] -->|SHA-256| Hash[32字节摘要]

Hash -->|添加OID前缀| Digest[DigestInfo]
Digest -->|填充Padding| Block["00 01 FF...FF 00 DigestInfo"]

Block -->|转大整数| IntM[整数 m]
Data & IntM -->|核心算法| Math["s = m^d mod n"]

Math -->|转字节 + Base64| Result([签名结果])

三、 核心实力代码 (Python)

以下代码剔除了辅助工具类,保留了解决报错的关键逻辑

  1. _generate_id_token:复刻了非标 JWT 签名流程。
  2. execute:处理了 return_url、KID 生成及 Payload 结构嵌套问题。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import json, time, base64, hashlib

class SsoTokenService:
ENCODING = "utf-8"
# 厂商公钥 (用于生成KID)
PUBLIC_KEY_STR = """-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"""
# 厂商私钥 (用于签名)
PRIVATE_KEY_STR = """-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"""

def execute(self, **kwargs):
"""主执行逻辑"""
# 1. 处理跳转地址 (关键点:参数名必须是 return_url)
# 优先使用传入参数,否则使用硬编码的已验证长链接兜底
DEFAULT_URL = "https%3A%2F%2Fwww.example.com%2Fportal..."
target_url = kwargs.get("return_url") or DEFAULT_URL
# (此处省略防止二次编码的逻辑...)

# 2. 构造 Payload (关键点:类型匹配与结构嵌套)
# Java端要求 exp/iat 为 String,且 cls 必须嵌套
payload_dict = {
"iss": kwargs.get("iss", "default_iss"),
"sub": kwargs.get("sub", "user@email.com"),
"aud": kwargs.get("aud", "tenant_id"),
"exp": str(int(time.time()) + 900), # 转String
"iat": str(int(time.time())), # 转String
"cls": kwargs.get("cls", {"appid": "100"}) # 必须嵌套
}

# 3. 生成 KID (关键点:保留PEM头尾标签进行Hash)
kid = hashlib.sha256(self.PUBLIC_KEY_STR.replace("\n", "").replace("\r", "").encode()).hexdigest()

header_json = json.dumps({"alg": "RS256", "kid": kid}, separators=(',', ':'))
payload_json = json.dumps(payload_dict, separators=(',', ':'))

# 4. 生成 Token (调用非标逻辑)
id_token = self._generate_id_token(header_json, payload_json)

return f"https://sso.example.com/Auth?id_token={id_token}&return_url={target_url}"

def _generate_id_token(self, header_str, payload_str):
"""
[核心算法] 复刻 Java 非标 JWT 生成流程
"""
# 1. 先转为【标准】Base64 (含 + / =)
std_h = base64.b64encode(header_str.encode()).decode()
std_p = base64.b64encode(payload_str.encode()).decode()

# 2. 拼接 (注意:对标准Base64串进行拼接)
sign_content = f"{std_h}.{std_p}"

# 3. 签名 (得到标准Base64签名)
std_sig = self._sign_rsa_manual(sign_content)

# 4. 最后统一转为 URL-Safe (替换字符,去等号)
to_safe = lambda s: s.replace('+', '-').replace('/', '_').replace('=', '')
return f"{to_safe(std_h)}.{to_safe(std_p)}.{to_safe(std_sig)}"

def _sign_rsa_manual(self, content):
"""
[零依赖] 纯 Python 实现 RSA SHA256 签名 (s = m^d mod n)
"""
# (此处省略 ASN.1 解析 n, d 的代码...)
# n, d = self._parse_pem(self.PRIVATE_KEY_STR)

# 1. SHA256 摘要
msg_hash = hashlib.sha256(content.encode()).digest()

# 2. PKCS#1 v1.5 填充 (含 OID)
oid = b'\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'
# padding 逻辑...

# 3. 核心数学运算
m_int = int.from_bytes(padding, 'big')
s_int = pow(m_int, d, n)

return base64.b64encode(s_int.to_bytes(key_len, 'big')).decode()

四、 避坑总结

  1. 协议对齐:不要盲目信任通用库(如 pyjwt),当服务端抛出空指针异常时,极大概率是 Base64 编码方式或 JSON 结构不匹配。
  2. 拥抱底层:在 FaaS 环境下,手写 RSA 算法虽然硬核,但能带来最佳的兼容性和零依赖体验。
  3. 细节决定成败:参数名(return_url vs redirect_url)、URL 编码次数、KID Hash 规则,任何一个细节的偏差都会导致 SSO 链路断裂。
Next:
使用 LLDB 动态调试提取 SQLCipher 数据库密钥