第4章 JWT断言生成与签名
4.1 原理概述
JWT(JSON Web )断言是 Google Play API 服务账号认证流程中的核心凭证。客户端使用 RSA SHA-256(RS256)算法对特定结构的 JSON 载荷进行签名,生成一个自包含的 ,用于向 Google OAuth 2.0 令牌端点换取访问令牌(access )。
JWT 断言由三个部分组成:Header(声明签名算法和类型)、Payload(声明签发者、目标受众、有效期等)、Signature(使用私钥对前两部分签名后的摘要)。三者以 Base64URL 编码后用点号(.)拼接,形成最终 。
关键公式:
JWT = Base64URL(Header) + "." + Base64URL(Payload) + "." + Base64URL(Signature)
JWT = Base64URL(Header) + "." + Base64URL(Payload) + "." + Base64URL(Signature)
4.2 流程步骤
- 加载私钥:从 P12 或 JSON 服务账号文件中读取 RSA 私钥。
- 构造 Header:固定
alg="RS256",typ="JWT"。 - 构造 Payload:设置
iss(服务账号邮箱)、scope(API 权限)、aud(固定为https://oauth2.googleapis.com/)、exp(过期时间)和iat(签发时间)。 - 生成签名:使用 SHA256 对 “Header.Payload” 字节做 RSA 签名。
- 拼接 :将三部分 Base64URL 编码后拼接为最终的 JWT 断言。
4.3 Header 与 Payload 字段规范
| 字段 | 所在部分 | 值/示例 | 说明 |
|---|---|---|---|
alg |
Header | "RS256" |
签名算法,必填 |
typ |
Header | "JWT" |
令牌类型,必填 |
iss |
Payload | "your-sa@project.iam.gserviceaccount.com" |
签发者(服务账号邮箱) |
scope |
Payload | "https://www.googleapis.com/auth/androidpublisher" |
请求的 API 权限,多个用空格分隔 |
aud |
Payload | "https://oauth2.googleapis.com/" |
目标受众,固定值 |
exp |
Payload | 当前时间 + 3600s 的 Unix 时间戳 | 过期时间,最长 1 小时 |
iat |
Payload | 当前时间的 Unix 时间戳 | 签发时间 |
4.4 代码实现(Python)
使用 google-auth 库或标准 PyJWT 库均可。以下示例使用 PyJWT 手动构造并签名:
import jwt
import time
from cryptography.hazmat.primitives import serialization
# 从 JSON 服务账号文件加载私钥
with open("service-account.json", "r") as f:
sa_info = json.load(f)
private_key_pkcs8 = sa_info["private_key"]
# 构造 Header
headers = {
"alg": "RS256",
"typ": "JWT"
}
# 构造 Payload
now = int(time.time())
payload = {
"iss": sa_info["client_email"],
"scope": "https://www.googleapis.com/auth/androidpublisher",
"aud": "https://oauth2.googleapis.com/",
"exp": now + 3600,
"iat": now
}
# 签名生成 JWT
jwt_ = jwt.encode(
payload=payload,
key=private_key_pkcs8,
algorithm="RS256",
headers=headers
)
print(f"生成的 JWT 断言:\n{jwt_}")
4.5 代码实现(Java)
使用 io.jsonwebtoken:jjwt-api 库实现:
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
// 从文件读取 PKCS8 私钥
String privateKeyPEM = new String(Files.readAllBytes(Paths.get("key.pem")))
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(privateKeyPEM);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(spec);
// 构造并签名 JWT
String jwt = Jwts.builder()
.setHeaderParam("alg", "RS256")
.setHeaderParam("typ", "JWT")
.setIssuer("your-sa@project.iam.gserviceaccount.com")
.setAudience("https://oauth2.googleapis.com/")
.setExpiration(new Date(System.currentTimeMillis() + 3600_000))
.setIssuedAt(new Date())
.claim("scope", "https://www.googleapis.com/auth/androidpublisher")
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
System.out.println("JWT: " + jwt);
4.6 调试与验证
- 使用 jwt.io 在线调试器:将生成的 粘贴到 Debugger 中,可查看 Header 和 Payload 解码后的内容,验证签名是否通过。
- 本地验证签名:使用公钥验证 JWT 签名的合法性:
# Python 验证示例
import jwt
public_key = sa_info["private_key"] # 实际上应使用公钥
# 真实场景建议从 Google 获取公钥,此处仅作格式展示
decoded = jwt.decode(jwt_, options={"verify_signature": False})
print("Payload:", decoded)
提示:在开发阶段,可以设置
exp 为当前时间 + 10 分钟避免频繁生成。生产环境建议使用 30~60 分钟并缓存 。
4.7 常见错误与排查
| 错误现象 | 根因 | 解决办法 |
|---|---|---|
| JWT 签名验证失败 | 私钥格式不正确,或 Header/Payload 在签名前后不一致 | 确保私钥为 PKCS8 格式,去除多余换行和注释;检查 Base64URL 编码无多余字符 |
Google 返回 invalid_grant |
Payload 中 iss 或 scope 与 Google Cloud Console 配置不符 |
验证服务账号邮箱、API 权限是否已授权,且 aud 是否为固定端点 |
时间戳偏差导致 exp 已过期 |
客户端系统时间不准确,或 exp 设置过短 |
使用 NTP 同步时间,设置 exp 为当前时间 + 3600 秒 |
Header 缺少 typ 字段 |
某些 JWT 库默认不添加 typ |
手动在 headers 参数中指定 "typ": "JWT" |
4.8 性能与稳定性优化
- 缓存:JWT 生成后可用于换取 access ,access 有效期通常 1 小时。建议将 access 缓存到内存或 Redis,避免每次请求都重新生成 JWT。
- 私钥复用:确保私钥对象在应用生命周期内只加载一次,避免反复 IO 开销。
- 时钟同步:生产服务器必须启用 NTP 服务,防止因时钟偏移导致 JWT 过期验证失败。
- 异步签名:在高并发场景下,使用线程池或异步 IO 执行签名操作,避免阻塞主线程。
安全警告:私钥文件不能提交到版本控制系统(Git)。建议使用密钥管理服务(KMS)或环境变量注入方式加载私钥,并设置文件权限为 400。
4.9 最终输出示例
生成的 JWT 断言形如:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ5b3VyLXNhQHByb2plY3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvYW5kcm9pZHB1Ymxpc2hlciIsImF1ZCI6Imh0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwiZXhwIjoxNzE5MDAwMDAwLCJpYXQiOjE3MTg5OTY0MDB9.
sxL2_8g9nJfQ3mZ_Yq5vGKzL0cX7d0cW9f6R0XhUc0gxVpN0uK2bQ4mY8s7dF5hJkL2vR6tY9p0
将此断言 POST 到 https://oauth2.googleapis.com/ 即可换取 access_。