Apple ID 第三方授权登录算法深度解析:从 SRP6a 到 Federate Token 的完整链路
一、前言:为什么研究 Apple ID 授权?
最近在做一个支持 "使用 Apple 登录"(Sign in with Apple) 的第三方应用时,发现国内对这块的技术分享非常少。Apple 的授权链路不像微信、QQ 那样有现成 SDK 调用,它走的是一套 类 SRP 零知识证明 + Federated Token 交换 的混合方案,整个流程涉及到密码学协商、签名校验、设备指纹等多个环节。
这篇文章会从协议层面拆解整个授权登录的完整链路,重点分析几个关键节点,并给出我们在逆向过程中总结出来的一些"经验值"。本文仅用于技术研究与安全测试,请勿用于任何非法用途。
二、整体流程概览
一次完整的 Apple ID 授权登录,通常包含以下几个阶段:
┌─────────────┐
│ 客户端发起 │ (传入 Apple ID / 手机号)
└──────┬──────┘
▼
┌─────────────┐
│ SRP 密钥协商 │ ← get_encrypted_a / get_encrypted_b
└──────┬──────┘
▼
┌─────────────┐
│ 密码校验通过 │
└──────┬──────┘
▼
┌─────────────┐
│ 获取会话凭据 │ ← signin/init / signin/complete
└──────┬──────┘
▼
┌─────────────┐
│ Federate交换 │ ← auth_federate → grant_code
└──────┬──────┘
▼
┌─────────────┐
│ 换取访问令牌 │ ← token exchange
└─────────────┘
下面我们逐个阶段展开。
三、SRP6a 零知识证明协商
Apple 的登录底层使用的是 SRP-6a 协议(Secure Remote Password, 6a 版本),这是 RFC 5054 中定义的标准协议。客户端不会把密码明文发到服务端,而是通过零知识证明让服务端"相信"你知道密码。
3.1 为什么 Apple 不直接传密码?
- 避免中间人拿到明文密码
- 服务端数据库泄露也不会暴露用户密码
- 即便全程被嗅探,也无法重放
3.2 关键参数协商
客户端首先要调用一个 get_enctypted_a 之类的接口,提交自己的公钥 A(基于随机数 a 生成)。服务端会返回 B、salt、以及一些协议参数。
我们抓到的一个典型请求伪代码如下:
# 客户端发起 SRP 协商
session_a = random_int(256) # 私钥 a
A = pow(g, session_a, N) # 公钥 A = g^a mod N
# 提交给 Apple
post("/get_enctypted_a", json={
"a": base64(A),
"account": apple_id,
# ... 协议版本、设备指纹等
})
这里的
N(大素数)和g(生成元)是 SRP 协议公开参数,Apple 使用的 1024-bit 安全素数,理论上不可爆破。
3.3 服务端响应处理
正常情况下服务端会返回 B 和 salt,但在网络环境差或 IP 被风控时,常见两种异常:
| 状态码 | 含义 | 我们的处理 |
|---|---|---|
| 200 | 返回 B 和 salt | 继续走 proof 校验 |
| 503 | 服务暂不可用(IP 被限流) | 切换代理 + 指数退避 |
| 400 | 参数异常 | 检查 a 是否为 0 |
| 401 | 账号密码错误 | 直接终止 |
经验之谈:这里的 503 几乎 100% 是 IP 维度的风控,不是账号维度。所以在做批量场景时,每个并发必须使用不同的高匿代理,且建议池子规模在 50 以上。
四、signin/complete 与会话凭据
SRP 校验通过后,Apple 会下发一个会话级别的凭据(不是最终的 token),这个阶段对应 signin/complete 接口,调用成功后会返回一个 grant_code(也叫 c)。
这是整个链路里最敏感的一步,直接决定了后续能不能拿到联邦令牌。
# signin/complete 伪代码
response = session.post(
"https://signin.apple.com/api/1.0/signin/complete",
json={
"account": apple_id,
"protocol": "s2k",
"m1": M1, # 客户端 proof
"c": session_id, # 协商阶段拿到的会话id
"createSession": True
}
)
grant_code = response.json()["authorizationCode"]
注意几个坑:
- IP 并发敏感性极高:
signin/complete对同 IP 的并发请求极其敏感,实测 concurrency > 2 就会出现 503。 - 必须携带协议版本号:低版本的
protocol字段会被拒绝。 - 客户端 proof
M1算错就直接 401,没有重试机会。
五、auth_federate 与联邦令牌
拿到 grant_code 后,调用 Apple 的 auth_federate 接口,将 Apple 账号的会话凭据转换为第三方应用可用的 OAuth token。
# 联邦令牌交换
resp = session.post(
"https://appleid.apple.com/auth/federate",
json={
"authorizationCode": grant_code,
"clientId": YOUR_CLIENT_ID,
"redirectUri": YOUR_REDIRECT,
}
)
federated_token = resp.json()["access_token"]
这个阶段有几个反爬特征:
- 请求必须携带
sign、x-helios、x-medusa等自定义头 - 这几个头的生成涉及 HMAC-SHA256 + 时间戳 + 设备指纹,具体算法各家逆向结果不一样(属于 App 风控的"看门狗")
- 缺失或错误任何一个,直接 401,不给具体原因
篇幅原因,这里就不展开签名头的具体生成了。感兴趣的可以自己抓包对比几次正常请求的差异,规律其实不难找。
六、passport 子流程:手机绑定与验证码
部分业务场景下,Apple 会要求二次验证(手机验证码),这套逻辑走的是独立的 passport 子域。流程大致是:
send_code → 加密手机号 → 平台下发短信 → 用户回填 → verify
加密方式比较有意思,伪代码如下:
# 加密手机号(伪代码)
def encrypt_mobile(phone: str) -> str:
# XOR with a fixed byte, then prefix with a marker byte
encoded = bytes([0x2E]) + bytes(b ^ 0x05 for b in phone.encode())
return base64(encoded)
# 加密验证码(伪代码)
def encrypt_code(code: str) -> str:
encoded = bytes(b ^ 0x05 for b in code.encode())
return base64(encoded)
加密常量在不同版本间可能会变化,实测有遇到 0x05 改成 0x07 的情况,做生产化方案时需要做兼容。
七、批量场景下的工程化经验
最后分享几个我们在工程化时踩过的坑:
- 代理质量 > 代理数量:与其用 1000 个烂代理,不如用 50 个高质量 SOCKS5 高匿代理。
- 指数退避是必须的:502/503/504/429 一律
2s → 4s → 8s退避,最多 3 次。 - 失败账号不要自动重试:Apple 短时间内对同一账号重试会触发风控,让用户手动重置状态比自动重试安全得多。
- session 必须绑死代理:不要多个 worker 共享一个
requests.Session,否则代理一掉全军覆没。 - 本地调试必须旁路代理:调用本机服务(
localhost:3000的加密服务)时一定要proxies={'http': '', 'https': ''},否则会被污染。
八、总结
Apple ID 的授权链路从协议设计上是非常优雅的——SRP 零知识证明 + Federate Token 交换,既保护了用户密码,又给了第三方受控的访问能力。但工程化落地时,反爬策略、IP 风控、签名头校验 这三座大山才是真正难点。
本文只展示了整体框架和部分关键节点的脱敏代码,如果你对:
- 完整的 SRP6a 协商实现
sign/x-helios/x-medusa签名头生成原理- 批量场景下的稳定代理调度方案
- 苹果设备风控对抗
感兴趣,欢迎评论区留言或私信交流,可以分享更多实战细节和踩坑记录。
浙公网安备 33010602011771号