第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)

4.2 流程步骤

  1. 加载私钥:从 P12 或 JSON 服务账号文件中读取 RSA 私钥。
  2. 构造 Header:固定 alg="RS256",typ="JWT"。
  3. 构造 Payload:设置 iss(服务账号邮箱)、scope(API 权限)、aud(固定为 https://oauth2.googleapis.com/)、exp(过期时间)和 iat(签发时间)。
  4. 生成签名:使用 SHA256 对 “Header.Payload” 字节做 RSA 签名。
  5. 拼接 :将三部分 Base64URL 编码后拼接为最终的 JWT 断言。
JWT 断言生成流程图(RS256) ① 加载 RSA 私钥 ② 构造 Header ③ 构造 Payload ④ RS256 签名 ⑤ Base64URL 编码 ⑥ 拼接 JWT 输出格式:header.payload.signature 各部分使用点号(.)连接,签名确保断言不可篡改

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_。