验证参数

可用的验证参数有 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. 初学jQuery使用方法

    jQuery引用 <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></scri ...

  2. iOS 编译过程原理(2)

    一.前言 <iOS编译过程的原理和应用>文章介绍了 iOS 编译相关基础知识和简单应用,但也很有多问题都没有解释清楚: Clang 和 LLVM 究竟是什么 源文件到机器码的细节 Link ...

  3. NSArray、NSDictionary

    一.NSDictionary 1.1 使用自定义对象 key Dictionaries manage pairs of keys and values. A key-value pair within ...

  4. 树莓派 Raspberry PI基础

    树莓派 Raspberry PI基础 官网网址:https://www.raspberrypi.org 下载地址:https://www.raspberrypi.org/downloads/ 官方系统 ...

  5. 使用xlsxwriter模块和xlrd模块操作Excel表格

    1.xlsxwriter模块介绍:主要用来生成excel表格,插入数据.插入图标等表格操作 如下代码:目的是往demo01.xlsx插入数据和图片 # 导入xlsxwriter模块:主要用来修改表格的 ...

  6. linux下使用笔记本的相关设置

    目录 无线连接 Wi-Fi 蓝牙 触摸板 电源管理 电源管理工具 电源相关行为的响应动作 按键和盖子的响应动作 电池低电量的响应动作 处理器调整 调频工具 关闭睿频 intel_pstate 休眠配置 ...

  7. 大数据篇:Hbase

    大数据篇:Hbase Hbase是什么 Hbase是一个分布式.可扩展.支持海量数据存储的NoSQL数据库,物理结构存储结构(K-V). 如果没有Hbase 如何在大数据场景中,做到上亿数据秒级返回. ...

  8. Java第四天,随机数如何生成?ArrayList如何使用?

    虽然很多时候我们查阅Java API文档,但是对于一些必要的类,我们还是需要去了解的.因为这样的情况下,我们就可以尽量的去缩短开发的周期.接下来我们认识一下哪些API类是必须熟记的. Random 这 ...

  9. 工作中常用的Android系统ADB命令收集

    ​工作中常用的Android系统ADB命令收集如下:先收藏以备以后查阅! ​ adb --help //adb帮助 adb start-server //启动adb server adb kill-s ...

  10. vue-resource安装与使用

    vue-resource是vue中使用的请求网络数据的插件,这个插件是依赖于vue的,简单说就是用来调接口的. 安装 cd 项目目录 npm i vue vue-resource --save-dev ...