验证参数

可用的验证参数有 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-----\r\n" .
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);
}
} }

苹果登录服务端JWT算法验证-PHP的更多相关文章

  1. (9)学习笔记 ) ASP.NET CORE微服务 Micro-Service ---- JWT算法

    一. JWT 简介 内部 Restful 接口可以“我家大门常打开”,但是如果要给 app 等使用的接口,则需要做权限校验,不能谁都随便调用. Restful 接口不是 web 网站,App 中很难直 ...

  2. java访问webservce,保持会话,服务端保存session验证

    在进行程序开发的过程中,遇到一个问题,怎么保持会话. 因为一帮进行方法调用很少涉及到即时身份验证的. 例如: 1:客户端登录后服务端保存登录用户信息: 2:客户端持有验证通过key再次请求: 3:服务 ...

  3. cas 单点登录服务端客户端配置

    首先,下载 cas-server-3.5.2-release http://pan.baidu.com/s/1GJ8Gs cas-client-3.2.1-release http://pan.bai ...

  4. iOS In-App Purchase(IAP)内购服务端二次验证注意事项

    前端iOS完成对应的商品购买之后,会得到一个Transaction(交易)的数据结构指针,后端实际上只需要这个结构内的一个东西,那就是 transaction.transactionReceipt. ...

  5. SSO-CAS实现单点登录服务端

    目录 CAS-SSO 一.单点登录-CAS 二.下载搭建CAS 1. 下载 CAS 5.3 2. 导入IDEA 3. 打包war 3. war包部署到Tomcat 4. 启动Tomcat,访问 htt ...

  6. 让 ASP.NET JS验证和服务端的 双验证 更简单

    只用JavaScript验证安全不安全谁都知道,答案是不安全,非常的不安全.因为在客户端进行的验证相当于“让用户自己验证自己”,很明显是不靠谱的.你不能避免一些恶意用户人为的修改自己的表单进行欺骗,也 ...

  7. [精华][推荐]CAS SSO单点登录服务端客户端学习

    1.通过下载稳定版本的方式下载cas的相关源码包,如下: 直接选择4.2.1的稳定代码即可 2.我们项目中的版本版本使用maven apereo远程库去下载 通过远程maven库下载cas-serve ...

  8. [精华][推荐]CAS SSO单点登录服务端客户端实例

    1.修改server.xml文件,如下: 注意: 这里使用的是https的认证方式,需要将这个配置放开,并做如下修改: <Connector port="8443" prot ...

  9. struts2(三)---struts2中的服务端数据验证框架validate

    struts2为我们提供了一个很好的数据验证框架–validate,该框架可以很方便的实现服务端的数据验证. ActionSupport类提供了一个validate()方法,当我们需要在某一个acti ...

随机推荐

  1. React Hooks 实现react-redux

    Redux 是目前 React 系统中最常用的数据管理工具,它落实并发扬了 Flux 的数据单向流动模式,被实践证明为一种成熟可用的模式. 尽管承受着一些非议,Redux 在 React 数据管理界的 ...

  2. 在操作Git Bash时出现的问题

    参考博客:https://blog.csdn.net/weixin_44394753/article/details/91410463 1.问题1 $ git remote add origin gi ...

  3. go中的面向对象总结

    我们总结一下前面看到的:Go 没有类,而是松耦合的类型.方法对接口的实现. OO 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢? 封装(数据隐藏):和别的 OO 语言有 ...

  4. 2017蓝桥杯贪吃蛇(C++C组)

    原题: 贪吃蛇长度+-------------------------------------------------+|                                        ...

  5. C语言 生日快乐

    #include <stdio.h> #include <math.h> #include <stdlib.h> #define I 20 #define R 34 ...

  6. javascript入门 之 ztree(三 简单json数据)

    <!DOCTYPE html> <HTML> <HEAD> <TITLE> ZTREE DEMO - Standard Data </TITLE& ...

  7. Google Adsense付款方式添加西联付款

    本文已同步到专业技术网站 www.sufaith.com, 该网站专注于前后端开发技术与经验分享, 包含Web开发.Nodejs.Python.Linux.IT资讯等板块. Google Adsens ...

  8. "小号文本"组件:<small> —— 快应用组件库H-UI

     <import name="small" src="../Common/ui/h-ui/text/c_tag_small"></impor ...

  9. matplotlib Transform

    2020-04-09 15:09:02 -- Edit by yangray Transform 类是TransformNode的子类,它是所有执行变换的TransformNode的实例的基类.所有非 ...

  10. not found 什么时候触发

    eq: BEGIN        DECLARE EXIT HANDLER FOR NOT FOUND SET o_state = 999;         select count(1) into ...