php Sign in with Apple(苹果授权登录PHP后端接口)

丁理
2023-12-01

详细配置参考:https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple

本文主要参考:

APP客户端授权登录功能开发不做讲述,主要记录PHP后端如何验证苹果授权登录。

针对后端验证苹果提供了两种验证方式,一种是基于JWT的算法验证,另外一种是基于授权码的验证,本文使用了JWT验证实现。

首先,APP客户端认证成功后会返回ASAuthorizationAppleIDCredential类型的对象,它的主要属性如下:

  • userID:授权的用户唯一标识,在一个开发者账户下的APP获取到的是一样的,类似微信开发API中的openid;
  • email、fullName:授权的用户资料,用户邮箱,昵称等信息;
  • authorizationCode:授权code
  • identityToken:授权用户的JWT凭证,用于验证信息合法性

其次,拿到这些信息,将user,email,fullName,identityToken等信息直接传递给后端,后端对请求进行验证(此处使用identityToken进行JWT验证):

  • identityTokenString实际上是JWT(JSON Web Token)格式的文件,JWT文件由三部分组成:
  1. Header
  2. Payload
  3. Signature

     这三部分由"."分割,其中Header和Payload是经过base64编码的。

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "AIDOPK1",
      "use": "sig",
      "alg": "RS256",
      "n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
      "e": "AQAB"
    }
  ]
}

kid,为密钥id标识,签名算法采用的是RS256(RSA 256 + SHA 256),kty常量标识使用RSA签名算法,其公钥参数为n和e,其值采用了BASE64编码,使用时需要先解码。

然后引入JWT算法,下面是主要逻辑及验证接口:

    /**
     * 第三方登录
     */
    public function appleIdLogin()
    {
        $openid = $this->params('userID', '');
        $verifyToken = $this->params('verifyToken', '');

        if(empty($openid) || empty($verifyToken)){
            $this->responseJson(1, '参数错误', array(), true);
        }

        //token校验
        $verifyRes = $this->appleJwtVerify($verifyToken);
        if(isset($verifyRes['jwtStatus']) && $verifyRes['jwtStatus'] == 'failed'){
             $this->responseJson(1, $verifyRes['jwtMsg'], array(), true);
             return;
        }else{
            //进行注册
            $res = $this->register('appleID', $openid);
        }
    }

    /**
     * AppleID登录JWT校验identityToken
     * 解析后的identityToken由三部分组成:Header.Payload.Signature
     *
     * @param string $identityToken
     * @return object
     * @throws \Exception
     */
    private function appleJwtVerify($identityToken = ''){        
        //获取Apple公钥访问地址:https://appleid.apple.com/auth/keys
        //得到Apple公钥:
        //{
        //    "kty": "RSA",
        //    "kid": "AIDOPK1",
        //    "use": "sig",
        //    "alg": "RS256",
        //    "n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
        //    "e": "AQAB"
        //}
        
        //通过Apple公钥在线(https://8gwifi.org/jwkconvertfunctions.jsp)得到用于解密的pem公钥字符串
        $publickKey = "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlxrwmuYSAsTfn+lUu4go
ZSXBD9ackM9OJuwUVQHmbZo6GW4Fu/auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD
4eRtY+RNwCWdjNfEaY/esUPY3OVMrNDI15Ns13xspWS3q+13kdGv9jHI28P87RvM
pjz/JCpQ5IM44oSyRnYtVJO+320SB8E2Bw92pmrenbp67KRUzTEVfGU4+obP5RZ0
9OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysy
d/JhmqX5CAaT9Pgi0J8lU/pcl215oANqjy7Ob+VMhug9eGyxAWVfu/1u6QJKePlE
+wIDAQAB
-----END PUBLIC KEY-----";//pem公钥 【也可以通过将RSA公钥modulus(N)和exponent(E)转换为PEM文件】
        
        $decoded = JWT::decode($identityToken, $publickKey, array('RS256'));
        return $decoded;
    }

    /**
     * 第三方登录账号注册
     *
     * @param string $regType 注册类型
     * @param string $openid 账户唯一识别
     * @throws \Blim\Exception\Stop
     */
    public function register($regType = '', $openid = ''){
        if(empty($regType) || empty($openid)){
            $this->responseJson(1, '注册失败', array(), true);
        }else{
            //注册逻辑
        }
    }

    /**
     * json输出
     * @param $code
     * @param string $message
     * @param array $exts
     * @param bool $hasData
     * @throws Stop
     */
    public function responseJson($code, $message = '', $exts = array(), $hasData = false)
    {
        $res = array('code' => $code, 'tips' => $message, 'msg' => $message);
        $res['data'] = array();
        if (!empty($exts)) {
            $res['data'] = $exts;
        }
        app('response')->withJson($res, 200);
        app()->stop();
    }

 

 类似资料: