验证参数
可用的验证参数有 userID、authorizationCode、identityToken,需要iOS客户端传过来
验证方式
苹果登录验证可以选择两种验证方式
具体可参考这篇文章 https://juejin.im/post/5e21c212f265da3e0640bf49
我们采用JWT算法校验 identityToken 的方式来验证
JWT算法原理
客户端传过来的userID示例 000327.cd00e3974ea8402dbe3a33e6867f1ee6.1006
identityToken 示例
eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnl3c3kuaW9zLmRlbW8iLCJleHAiOjE1ODY5NDY5NzAsImlhdCI6MTU4Njk0NjM3MCwic3ViIjoiMDAwMzI3LmNkMDBlMzk3NGVhODQwMmRiZTNhMzNlNjg2N2YxZWU2LjEwMDYiLCJjX2hhc2giOiJsQTFkcDlZMnZBVzlFQXlkSWw2MVh3IiwiZW1haWwiOiI5ZXpyMmszaDZzQHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTg2OTQ2MzcwLCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.GqZFKMQ3KTG42x2-W7r69nnYqqoBHszI4LBI7m7ysyqBRyt1XSGDPy440F153C8x05VgZgkYi0mIZheCIenIMl5R0unOKrXhvHihgwIKtuvPClRQmAyZYxOWct8xGoPvrRpZr4AkJwxauUxaY8NIoV8-UrNduQcjW8-63-wF9B0F-2p61WZuOEmCoULj2aW7fBoRgFylGbQpXAU_8t32fj1JG3OJzErDJsi1P1CJyKaamd-UpVmgwyaCl0nXMnX0CB0ERqb76M67BHY0ji3VBuIp3uZczEEJMzFtgAevOfgoNYRFicVBr25XoyaWYPxZgYnI-AeUQgvnwHaacx4bkg
使用JWT算法做验证,不需要authorizationCode。校验算法是对identityToken做处理的。
把identityToken 用 . 点号分割得到三个部分,前两个部分可以用base64_decode分别得到两串JSON信息。
第一段称为 header,描述了这段消息的加密方式
{"kid":"eXaunmL","alg":"RS256"}
第二段称为 payload,是消息的具体内容
{ "iss":"https://appleid.apple.com", "aud":"com.ywsy.ios.demo", "exp":1586946970, "iat":1586946370, "sub":"000327.cd00e3974ea8402dbe3a33e6867f1ee6.1006", "c_hash":"lA1dp9Y2vAW9EAydIl61Xw", "email":"9ezr2k3h6s@privaterelay.appleid.com", "email_verified":"true", "is_private_email":"true", "auth_time":1586946370, "nonce_supported":true }
校验流程
1、解析出identityToken的第二段信息,即payload;
2、检查userID与payload中的sub字段是否一致;
3、检查payload中的exp字段,有效期时间戳是否已过期;
4、从苹果服务器读取公钥;
5、苹果公钥转为pem格式;
6、使用pem公钥校验identityToken;
其中第4步,从苹果服务器读取公钥 https://appleid.apple.com/auth/keys 得到一串JSON
{ "keys": [ { "kty": "RSA", "kid": "86D88Kf", "use": "sig", "alg": "RS256", "n": "iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ", "e": "AQAB" }, { "kty": "RSA", "kid": "eXaunmL", "use": "sig", "alg": "RS256", "n": "4dGQ7bQK8LgILOdLsYzfZjkEAoQeVC_aqyc8GC6RX7dq_KvRAQAWPvkam8VQv4GK5T4ogklEKEvj5ISBamdDNq1n52TpxQwI2EqxSk7I9fKPKhRt4F8-2yETlYvye-2s6NeWJim0KBtOVrk0gWvEDgd6WOqJl_yt5WBISvILNyVg1qAAM8JeX6dRPosahRVDjA52G2X-Tip84wqwyRpUlq2ybzcLh3zyhCitBOebiRWDQfG26EH9lTlJhll-p_Dg8vAXxJLIJ4SNLcqgFeZe4OfHLgdzMvxXZJnPp_VgmkcpUdRotazKZumj6dBPcXI_XID4Z4Z3OM1KrZPJNdUhxw", "e": "AQAB" } ] }
关键步骤要做的就是把 “kid”:"eXaunmL" 的这部分JSON拿出来,用其n值和e值构造出苹果pem格式的公钥。
JWT算法函数,涉及密码数学知识,不详解
<?php static function createPemFromModulusAndExponent($n, $e) static function urlsafeB64Decode($input) static function encodeLength($length)
核心代码
<?php class Common_Apple{ protected static $supported_algs = array( 'HS256' => array('hash_hmac', 'SHA256'), 'HS512' => array('hash_hmac', 'SHA512'), 'HS384' => array('hash_hmac', 'SHA384'), 'RS256' => array('openssl', 'SHA256'), 'RS384' => array('openssl', 'SHA384'), 'RS512' => array('openssl', 'SHA512'), ); function __construct($game_id){} function get_login_info($userID, $authorizationCode, $identityToken){ /*{{{*/ $token = explode('.', $identityToken); $jwt_header = json_decode( base64_decode($token[0]), TRUE); $jwt_data = json_decode( base64_decode($token[1]), TRUE); $jwt_sign = $token[2]; // var_dump($jwt_header); // var_dump($jwt_data); // var_dump($jwt_sign); if( $userID !== $jwt_data['sub']){ return fail('用户ID与token不对应'); } if( PRODUCTION_ENV && $jwt_data['exp'] < time() ){ return fail('token已过期,请重新登录'); } $applekeys = Common_Http::get_https_content('https://appleid.apple.com/auth/keys'); $applekeys = json_decode($applekeys, TRUE); // var_dump($applekeys); if( !$applekeys ){ return fail('请求苹果服务器失败'); } $the_apple_key = []; foreach($applekeys['keys'] as $key){ if($key['kid'] == $jwt_header['kid'] ){ $the_apple_key = $key; } }unset($key); // var_dump($the_apple_key); $pem = self::createPemFromModulusAndExponent($the_apple_key['n'], $the_apple_key['e']); $pKey = openssl_pkey_get_public($pem); // var_dump($pKey); if( $pKey === FALSE ){ return fail('生成苹果pem失败'); } $publicKeyDetails = openssl_pkey_get_details($pKey); // var_dump($publicKeyDetails); $pub_key = $publicKeyDetails['key']; $alg = $jwt_header['alg']; $ok = self::verify("$token[0].$token[1]", static::urlsafeB64Decode($jwt_sign), $pub_key, $alg); // var_dump($ok); if( !$ok ){ return fail('苹果登录签名校验失败'); } return success([]); /*}}}*/ } /** * * Create a public key represented in PEM format from RSA modulus and exponent information * * @param string $n the RSA modulus encoded in Base64 * @param string $e the RSA exponent encoded in Base64 * @return string the RSA public key represented in PEM format */ protected static function createPemFromModulusAndExponent($n, $e) { $modulus = static::urlsafeB64Decode($n); $publicExponent = static::urlsafeB64Decode($e); $components = array( 'modulus' => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus), 'publicExponent' => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent) ); $RSAPublicKey = pack( 'Ca*a*a*', 48, self::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent'] ); // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA $RSAPublicKey = chr(0) . $RSAPublicKey; $RSAPublicKey = chr(3) . self::encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey; $RSAPublicKey = pack( 'Ca*a*', 48, self::encodeLength(strlen($rsaOID . $RSAPublicKey)), $rsaOID . $RSAPublicKey ); $RSAPublicKey = "-----BEGIN PUBLIC KEY----- " . chunk_split(base64_encode($RSAPublicKey), 64) . '-----END PUBLIC KEY-----'; return $RSAPublicKey; } /** * Decode a string with URL-safe Base64. * * @param string $input A Base64 encoded string * * @return string A decoded string */ protected static function urlsafeB64Decode($input) { $remainder = strlen($input) % 4; if ($remainder) { $padlen = 4 - $remainder; $input .= str_repeat('=', $padlen); } return base64_decode(strtr($input, '-_', '+/')); } /** * DER-encode the length * * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. * * @access private * @param int $length * @return string */ protected static function encodeLength($length) { if ($length <= 0x7F) { return chr($length); } $temp = ltrim(pack('N', $length), chr(0)); return pack('Ca*', 0x80 | strlen($temp), $temp); } /** * Get the number of bytes in cryptographic strings. * * @param string * * @return int */ protected static function safeStrlen($str) { if (function_exists('mb_strlen')) { return mb_strlen($str, '8bit'); } return strlen($str); } /** * Verify a signature with the message, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. * * @param string $msg The original message (header and body) * @param string $signature The original signature * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key * @param string $alg The algorithm * * @return bool * * @throws DomainException Invalid Algorithm or OpenSSL failure */ protected static function verify($msg, $signature, $key, $alg) { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } list($function, $algorithm) = static::$supported_algs[$alg]; switch($function) { case 'openssl': $success = openssl_verify($msg, $signature, $key, $algorithm); if ($success === 1) { return true; } elseif ($success === 0) { return false; } // returns 1 on success, 0 on failure, -1 on error. throw new DomainException( 'OpenSSL error: ' . openssl_error_string() ); case 'hash_hmac': default: $hash = hash_hmac($algorithm, $msg, $key, true); if (function_exists('hash_equals')) { return hash_equals($signature, $hash); } $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); $status = 0; for ($i = 0; $i < $len; $i++) { $status |= (ord($signature[$i]) ^ ord($hash[$i])); } $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); return ($status === 0); } } }