• 苹果登录服务端JWT算法验证-PHP


    验证参数

    可用的验证参数有 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);
            }
        }
    
    
    }
  • 相关阅读:
    Bootstrap
    格式化字符串
    闭包函数与装饰器
    正则表达式
    jQuery
    分布式-锁-1.1 多线程锁无法满足的场景
    effective python 读书笔记-第22条: 尽量用辅助类来维护程序的状态,而不要用字典
    effective python 读书笔记:第21条-用只能以关键字形式指定的参数来确保代码明晰
    effective python 读书笔记:第20条-用None和文档字符串来描述具有动态默认值的参数
    git如何将上游(upstream)新建分支(origin没有)导入到origin中?
  • 原文地址:https://www.cnblogs.com/batsing/p/sign-in-with-apple.html
Copyright © 2020-2023  润新知