PHP 第39章 苹果登录后端验证 PHP 第39章 苹果登录后端验证

2024-11-11

一、为什么需要苹果登录

根据苹果审核指南要求,如果 App 使用第三方或社交登录服务 (例如,Facebook 登录、Google 登录、通过 Twitter 登录、通过 LinkedIn 登录、通过 Amazon 登录或微信登录) 来对其进行设置或验证这个 App 的用户主帐户,则该 App 必须同时提供“通过 Apple 登录”作为同等选项。

详情参考:

https://developer.apple.com/cn/app-store/review/guidelines/#sign-in-with-apple

二、验证苹果登录的身份的两种方法

第一种是通过验证JWT(JSON Web Token)的 identityToken,涉及到 JWT 的结构、苹果公钥的获取以及 PHP 的 JWT 库使用。

什么是 JWT:

https://lulublog.cn/p/idikS4

第二种方法是验证 authorizationCode,需要私钥文件、KeyID 和 TeamID,通过苹果接口进行验证并获取用户信息。整个过程包括验证流程、所需参数及错误处理。

2.1、第一种方法:验证 identityToken

APP 端登录成功后,会拿到一个 identityToken 的字段,里面的值是一个 JWT,以.符号分隔,分为header、payload、signature 三部分,类似长这样:

xxxx.yyyy.zzzz

APP 需要把这个 identityToken 传给后端,后端进行验证。JWT 里的 signature 部分是苹果使用私钥对其进行的签名,要验证这个签名,需要先获得苹果的公钥,而公钥可以通过 JWKS(JSON Web Key Set)来转换获得。获取 JWKS 的接口是

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

https://file.lulublog.cn/images/3/2024/11/qabOaZ0j8AG56t5z6NjJj80TO0Arh0.jpg

接口返回的内容如上图,可以看到目前苹果有四个 JWK(后面可能会随时变更,因此不能写死在服务端,必须通过接口获取!)。然后我们需要用到一个名为 php-jwt 的第三方库,使用 composer 命令安装:

composer require firebase/php-jwt

安装完后可以使用这个库来验证 JWT 签名和获取 payload 里的信息了,代码如下:

require_once(__DIR__ . "/vendor/autoload.php");

use Firebase\JWT\JWK;
use Firebase\JWT\JWT;

// APP登录成功后获得的identityToken
$identityToken = 'xxxx.yyyy.zzzz';

// 调用苹果接口获取JWKS
$apiResponse = file_get_contents('https://appleid.apple.com/auth/keys');
$jwkSet = json_decode($apiResponse, true);
if (!is_array($jwkSet)) {
    exit("Get JWK failed");
}

// 将JWKS转换为公钥
try {
    $pubKeys = JWK::parseKeySet($jwkSet);
} catch (\Exception $e) {
    exit("Parse key set failed:" . $e->getMessage());
}

// 使用公钥对JWT进行检验
try {
    $payload = JWT::decode($identityToken, $pubKeys, ['RS256']);
} catch (\Exception $e) {
    exit("Decode JWT failed:" . $e->getMessage());
}

// 若检验通过,会得到JWT里的payload信息
echo(json_encode($payload));

下图是 payload 里包含的信息例子:

https://file.lulublog.cn/images/3/2024/11/SD77jrskZsD706MnfRs01kHdG0h0GF.png

获得 payload 信息后,还需要检查以下字段,确保:

  • iss 的值等于 https://appleid.apple.com

  • exp 是 JWT 的过期时间,当前时间不能大于 exp,也就是不能过期

  • aud 的值等于你的应用的 APP ID

全部通过后,就可以使用 sub 字段(用户的苹果账号 ID)和 email 字段(用户的邮箱地址)完成后面的登录逻辑啦。

2.2、第二种方法:验证 authorizationCode

APP 端登录成功后,除了会拿到 identityToken字段外,还可以拿到一个 authorizationCode 字段,下面介绍验证 authorizationCode 的方法。

验证 authorizationCode 需要有 p8 格式的私钥文件、私钥ID(Key ID)、Team ID 这三样东西,私钥文件可以在苹果开发者后台下载,Key ID 和 Team ID 也同样,见下图:

https://file.lulublog.cn/images/3/2024/11/e3G0JJJ69h7300j9COjcaH9D77J0jv.png

https://file.lulublog.cn/images/3/2024/11/F1vbLDPVeRN4i03XHLdpRi40350r0p.png

下载 p8 格式私钥后,使用下面命令转换为 pem 格式:

openssl pkcs8 -nocrypt -in AuthKey.p8 -out AuthKey.pem

最后通过苹果提供的接口来验证:

require_once(__DIR__ . "/vendor/autoload.php");

use Firebase\JWT\JWK;
use Firebase\JWT\JWT;

$clientId = ''; // APP的BUNDLE_ID
$authCode = ''; // APP登录成功后获得的authorizationCode
$teamId = ''; // Team ID
$keyId = ''; // Key ID
$privateKeyFilePath = "/path/to/AuthKey.pem"; // pem格式私钥文件的路径

// 使用私钥来生成client_secret
$privateKey = openssl_pkey_get_private("file://$privateKeyFilePath");
if (!$privateKey) {
   exit("Get private key failed");
}
$payload = [
   'iss' => $teamId,
   'iat' => time(),
   'exp' => time() + 86400,
   'aud' => 'https://appleid.apple.com',
   'sub' => $clientId,
];
$clientSecret = JWT::encode($payload, $privateKey, 'ES256', $keyId);

// 调用苹果接口验证authorizationCode
$postParams = [
   'client_id' => $clientId,
   'client_secret' => $clientSecret,
   'code' => $authCode,
   'grant_type' => 'authorization_code',
];

$ch = curl_init();
curl_setopt_array($ch, [
   CURLOPT_URL => 'https://appleid.apple.com/auth/token',
   CURLOPT_RETURNTRANSFER => true,
   CURLOPT_HEADER => false,
   CURLOPT_POST => true,
   CURLOPT_POSTFIELDS => http_build_query($postParams),
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

var_dump($httpCode, $response);

若验证成功,苹果会返回 200 HTTP 状态码,并且有返回一个 id_token 字段,里面是一个包含用户信息的 JWT,base64decode 后再 json_decode,就可以获得用户信息。

阅读 288