当前位置: 首页 > news >正文

PHP实现 Apple ID 登录的服务端验证指南

在 iOS 应用中启用 “通过 Apple 登录”(Sign In with Apple)后,客户端会获取一个 身份令牌(identity token)。该令牌是一个JWT(JSON Web Token),需要由服务端验证其真实性和完整性,然后才能用于在您的系统中创建或登录用户账户。下面我们详细说明 Apple 登录的完整服务端流程,以及如何在PHP中验证 Apple 返回的JWT,包括获取并缓存公钥、验证签名和重要的安全细节。

Apple 登录的服务端流程概览

  1. 客户端获取身份令牌:用户在 iOS 上通过Apple完成登录后,您的应用会收到一个 identityToken(以及一个 authorizationCode)。identityToken 实质上是一个JWT字符串,由三部分组成:header(头部)、payload(载荷)和signature(签名),以.分隔​。该JWT由Apple使用其私钥签名,包含了用户标识等信息。

  2. 传输给服务端:客户端将此 identityToken 发送给您的应用服务器。服务端需要验证该令牌确实由 Apple 签发且未被篡改。Apple没有提供类似Google那样的直接API来验证这个token,因此需要服务端自行完成JWT的验证。

  3. 服务端验证JWT签名:服务端从Apple提供的公开密钥中找到对应的公钥,验证JWT的签名是否有效​。只有使用Apple的公钥成功验证签名,才能确认该令牌确实由Apple签发​。

  4. 解码并校验载荷:签名验证通过后,解码JWT获取其中的payload数据,如用户的唯一标识(sub)、邮箱(email)等,并进一步校验关键字段的有效性(issuer、audience、过期时间、nonce等)​。

  5. 完成登录/注册:验证无误后,可使用JWT中的sub作为用户的Apple身份ID,在您数据库中查找或创建对应用户,并利用邮箱等信息完善用户数据。此后,即可认为该用户通过Apple登录得到认证。

提示:Apple还提供了使用authorizationCode换取令牌的OAuth接口,但本文主要聚焦于直接验证identityToken(JWT)的方案。

获取并缓存 Apple 公钥 (JWKS)

Apple 使用其私钥对 identityToken 进行签名,因此服务端需要通过 JWKS(JSON Web Key Set)接口获取 Apple 的公钥集合,并从中提取对应的公钥用于签名验证。JWKS 接口地址为:

https://appleid.apple.com/auth/keys

返回的数据结构类似于以下 JSON 格式:

{"keys": [{"kty": "RSA","kid": "ABCD1234EFGH","use": "sig","n": "...","e": "..."},...]
}

每个密钥包含kid(密钥标识)、alg(算法)、n(模数)、e(指数)等信息。我们需要根据JWT中header部分的kid来选择正确的公钥进行验证​。切勿将公钥信息硬编码在服务端,因为Apple可能不定期更换公钥或增加新的密钥​。应该每隔一段时间通过上述接口获取最新JWKS,并缓存这些公钥,以减少每次验证时的网络请求和延迟​,每条记录中的 kid 字段对应于签发 token 时使用的密钥 ID。需根据该字段匹配正确的公钥。

解析并验证 identityToken

客户端传递过来的 identityToken 是一个标准的 JWT 结构,由三部分组成:头部、载荷和签名。以下是具体步骤:

  1. 解码 JWT 并提取头部信息
    提取 kid 值以定位 JWKS 中的具体公钥。

  2. 加载 Apple 公钥
    将 JWKS 返回的公钥转换为适合验证的形式。

  3. 验证签名有效性
    利用 firebase/php-jwt 库调用方法验证签名是否合法。没有安装请安装
    composer require firebase/php-jwt

示例代码如下:

<?php
require_once __DIR__ . '/vendor/autoload.php';use Firebase\JWT\JWK;
use Firebase\JWT\JWT;// (1) 从客户端获取 Apple 身份令牌 (JWT 字符串)
$identityToken = $_POST['identityToken'] ?? '';  // 这里假设通过POST传入// (2) 获取Apple的JWKS公钥列表(可加缓存机制避免频繁请求)
$jwksJson = file_get_contents('https://appleid.apple.com/auth/keys');
$jwkSet = json_decode($jwksJson, true);
if (!$jwkSet || !isset($jwkSet['keys'])) {exit("无法获取Apple公钥 JWKS");
}// 将JWKS转换为公钥数组(使用kid索引)
$publicKeys = JWK::parseKeySet($jwkSet);  // 解析成功将返回以kid为键的公钥数组&#8203;:contentReference[oaicite:11]{index=11}try {// (3) 验证JWT签名并解码payload(指定只允许RS256算法)$payload = JWT::decode($identityToken, $publicKeys, ['RS256']);
} catch (\Exception $e) {// 签名验证或解码失败exit("令牌无效:" . $e->getMessage());
}// (4) JWT 验证通过,提取payload中的数据
$payloadData = (array)$payload;  // 转换StdClass为关联数组
// 例如:$payloadData 包含 iss, aud, exp, iat, sub, email 等字段// (5) 校验重要字段
if ($payloadData['iss'] !== 'https://appleid.apple.com') {exit("令牌签发者无效");
}
$clientId = '你的App的CLIENT_ID或Bundle ID';  // 开发者在Apple注册的服务ID
if ($payloadData['aud'] !== $clientId) {exit("令牌的受众不匹配");
}
if ($payloadData['exp'] < time()) {exit("令牌已过期");
}
if (isset($payloadData['nonce'])) {// 如果您的登录请求使用了nonce,则在此处验证 nonce 是否匹配$expectedNonce = '当初发送登录请求时的原始nonce或其哈希';if ($payloadData['nonce'] !== $expectedNonce) {exit("Nonce 验证失败");}
}// (6) 利用验证后的用户信息执行后续逻辑
$userId = $payloadData['sub'];        // Apple用户唯一ID
$userEmail = $payloadData['email'] ?? '';  // 用户的Email(可能是私隐中继邮件地址)

 构造 client_secret

在请求 Apple OAuth 认证的过程中,还需要提供一个名为 client_secret 的参数。这是一个自定义生成的 JWT,包含以下内容:

  • iss: 开发者团队 ID (TeamID)。
  • aud: 固定值 "https://appleid.apple.com"
  • sub: App Bundle Identifier。
  • iat: 当前时间戳。
  • exp: 过期时间戳(通常设置为 iat + 1800 秒)。

生成流程如下:

<?php
use Firebase\JWT\JWT;$teamId = 'YOUR_TEAM_ID';         // 替换为实际 Team ID
$clientSecretKeyId = 'KEY_ID';   // 替换为实际 Key ID
$bundleIdentifier = 'BUNDLE_ID'; // 替换为应用的 Bundle Identifier
$pemFilePath = './AuthKey.pem';   // 下载的私钥路径$header = ['alg' => 'ES256','kid' => $clientSecretKeyId,
];$payload = ['iss' => $teamId,'aud' => 'https://appleid.apple.com','sub' => $bundleIdentifier,'iat' => time(),'exp' => time() + 1800, // 设置有效期为半小时
];$keyContent = openssl_pkey_get_private(file_get_contents($pemFilePath));if (!$keyContent) {die("Failed to load private key");
}$clientSecret = JWT::encode($payload, $keyContent, 'ES256', null, $header);echo "Generated client_secret: {$clientSecret}";
?>

利用 ES256 算法生成了一个有效的 client_secret,供后续认证请求使用

请求访问令牌

向 Apple 发起 POST 请求以交换最终的访问令牌。URL 地址为:

https://appleid.apple.com/auth/token

所需参数包括但不限于:

  • grant_type: 固定值 "authorization_code".
  • code: 手机端传来的 authorizationCode.
  • redirect_uri: 注册的应用回调 URL.
  • client_id: App 的 Bundle Identifier.
  • client_secret: 上述生成的结果.

代码片段如下:

$data = http_build_query(['grant_type' => 'authorization_code','code' => $_POST['authorizationCode'],'redirect_uri' => 'REDIRECT_URI',     // 替换为实际重定向 URI'client_id' => 'BUNDLE_IDENTIFIER',   // 替换为实际 Bundle Identifier'client_secret' => $clientSecret,    // 上面生成的 client_secret
]);$options = ['http' => ['method' => 'POST','content' => $data,'header' => 'Content-Type: application/x-www-form-urlencoded'],
];$response = file_get_contents('https://appleid.apple.com/auth/token', false, stream_context_create($options));
var_dump(json_decode($response, true)); // 输出响应数据

相关文章:

  • 【Redis】服务端高并发分布式结构演进之路
  • PostSwigger 的 CSRF 漏洞总结
  • 《Learning Langchain》阅读笔记10-RAG(6)索引优化:MultiVectorRetriever方法
  • OpenSSH配置连接远程服务器MS ODBC驱动与Navicat数据库管理
  • C#学习第19天:多线程
  • 项目驱动 CAN-bus现场总线基础教程》随笔
  • C语言内敛函数
  • Redis故障防御体系:构建七层免疫系统的设计哲学
  • Selenium自动化测试+OCR-获取图片页面小说
  • OpenManus云端部署及经典案例应用
  • Monorepo、Lerna、Yarn Workspaces、pnpm Workspaces 用法
  • Revive 中的 Precompile 合约:实现与调用机制
  • Jetpack Room 使用详解
  • 【多模态模型】跨模态智能的核心技术与应用实践
  • 【误差理论与可靠性工程】蒙特卡洛法计算电路可靠度和三极管静态工作点电压
  • 新增 29 个专业,科技成为关键赛道!
  • 服务器不能复制粘贴文件的处理方式
  • 前端面试高频算法
  • AI服务器与普通服务器之间的区别
  • 电商数据采集电商,行业数据分析,平台数据获取|稳定的API接口数据
  • 普京发表声明感谢协助俄军收复库尔斯克州的朝鲜军人
  • 中消协发布“五一”消费提示:践行“光盘行动”,抵制餐饮浪费
  • 人民日报社论:做新时代挺膺担当的奋斗者
  • 新干式二尖瓣瓣膜国内上市,专家:重视瓣膜病全生命周期管理
  • 十四届全国人大常委会第十五次会议在京举行,审议民营经济促进法草案等
  • 独家丨申万宏源研究所将迎来新所长:首席策略分析师王胜升任