验证参数

可用的验证参数有 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. gold 30 mins

  2. jenkins集成sonarQube实现代码质量检查

    1.sonarQube的简介 SonarQube是一款自动化代码审查工具,用于检测代码中的错误.漏洞和代码异味.它可以与你现有的工作流集成,以支持跨项目分支和拉取请求的连续代码检查. 其工作流程如下: ...

  3. 1030 Travel Plan (30分)(dijkstra 具有多种决定因素)

    A traveler's map gives the distances between cities along the highways, together with the cost of ea ...

  4. Nginx知多少系列之(三)配置文件详解

    目录 1.前言 2.安装 3.配置文件详解 4.Linux下托管.NET Core项目 5.Linux下.NET Core项目负载均衡 6.Linux下.NET Core项目Nginx+Keepali ...

  5. python--Django(三)视图

    Django的视图 不同于其他语言的MVC模式,Django采用的是MVT模式,即Model.View.Template,这里的View其实质就是其他语言中的Controller(emmm.....) ...

  6. SpringMVC(六):分层整合SSM

    环境搭建 版本: MySQL 8 SpringMVC+Spring+Mybatis C3P0连接池 数据库建表 CREATE DATABASE `ssmbuild`; `ssmbuild` DROP ...

  7. node+mysql数据库连接(入门)

    node+mysql的数据库操作: 1 //引入mysql var mysql = require('mysql'); //进行数据库连接设置 var connection = mysql.creat ...

  8. Python库-Matplotlib

    Matplotlib官网https://matplotlib.org,Matplotlib是一个Python的2D绘图库. 可视化是整个数据分析的关键辅助工具,可以清晰的理解数据. 折线图(用于显示数 ...

  9. Python常见数据结构-Tuple元组

    Python Tuple基本特点 元组与列表类似,不同之处在于元组的元素不能修改. 与字符串和列表一样,可以根据下标进行切片索引. 元组使用小括号,单一元素的元组定义是必须加一个逗号. Python ...

  10. 廖雪峰 Git 教程 + Git-Cheat-Sheet 学习总结

    廖雪峰 Git 教程 + Git-Cheat-Sheet 学习总结 本教程主要是个人的 Git 学习总结. 主要参考博客: 廖雪峰 Git 教程 Git-Cheat-Sheet 文章目录 廖雪峰 Gi ...