苹果登录服务端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-----\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的更多相关文章
- (9)学习笔记 ) ASP.NET CORE微服务 Micro-Service ---- JWT算法
一. JWT 简介 内部 Restful 接口可以“我家大门常打开”,但是如果要给 app 等使用的接口,则需要做权限校验,不能谁都随便调用. Restful 接口不是 web 网站,App 中很难直 ...
- java访问webservce,保持会话,服务端保存session验证
在进行程序开发的过程中,遇到一个问题,怎么保持会话. 因为一帮进行方法调用很少涉及到即时身份验证的. 例如: 1:客户端登录后服务端保存登录用户信息: 2:客户端持有验证通过key再次请求: 3:服务 ...
- cas 单点登录服务端客户端配置
首先,下载 cas-server-3.5.2-release http://pan.baidu.com/s/1GJ8Gs cas-client-3.2.1-release http://pan.bai ...
- iOS In-App Purchase(IAP)内购服务端二次验证注意事项
前端iOS完成对应的商品购买之后,会得到一个Transaction(交易)的数据结构指针,后端实际上只需要这个结构内的一个东西,那就是 transaction.transactionReceipt. ...
- SSO-CAS实现单点登录服务端
目录 CAS-SSO 一.单点登录-CAS 二.下载搭建CAS 1. 下载 CAS 5.3 2. 导入IDEA 3. 打包war 3. war包部署到Tomcat 4. 启动Tomcat,访问 htt ...
- 让 ASP.NET JS验证和服务端的 双验证 更简单
只用JavaScript验证安全不安全谁都知道,答案是不安全,非常的不安全.因为在客户端进行的验证相当于“让用户自己验证自己”,很明显是不靠谱的.你不能避免一些恶意用户人为的修改自己的表单进行欺骗,也 ...
- [精华][推荐]CAS SSO单点登录服务端客户端学习
1.通过下载稳定版本的方式下载cas的相关源码包,如下: 直接选择4.2.1的稳定代码即可 2.我们项目中的版本版本使用maven apereo远程库去下载 通过远程maven库下载cas-serve ...
- [精华][推荐]CAS SSO单点登录服务端客户端实例
1.修改server.xml文件,如下: 注意: 这里使用的是https的认证方式,需要将这个配置放开,并做如下修改: <Connector port="8443" prot ...
- struts2(三)---struts2中的服务端数据验证框架validate
struts2为我们提供了一个很好的数据验证框架–validate,该框架可以很方便的实现服务端的数据验证. ActionSupport类提供了一个validate()方法,当我们需要在某一个acti ...
随机推荐
- ArrayList中的Iterator详解
每个实现Iterable接口的类必须提供一个iterator方法,返回一个Iterator对象,ArrayList也不例外 public Iterator<E> iterator() { ...
- [bzoj4472][树形DP] Salesman
题目 原地址 解说 刚看完这道题感觉还是挺乱的,可能那时候脑子不太清醒,一度觉得自己又要重拾Tarjan了.当然最后还是发觉应该用树形DP. (以下dp[u]代表以u为根的包括自己在内的子树的最大利润 ...
- Python——图像手绘效果
1.图像的RGB色彩模式 PIL PIL, Python Image Library PIL库是一个具有强大图像处理能力的第三方库 在命令行下的安装方法: pip install pillow fro ...
- Android进阶之AIDL的使用详解
原文首发于微信公众号:jzman-blog,欢迎关注交流! AIDL(Android 接口定义语言),可以使用它定义客户端与服务端进程间通信(IPC)的编程接口,在 Android 中,进程之间无法共 ...
- PostgreSql 自定义函数:批量调整某个字段长度
CREATE or replace FUNCTION alterColumn(cloumnName VARCHAR(32), out v_retcode text)AS$BODY$ declare r ...
- P3381 【模板】最小费用最大流(MCMF)
P3381 [模板]最小费用最大流 题目描述 如题,给出一个网络图,以及其源点和汇点,每条边已知其最大流量和单位流量费用,求出其网络最大流和在最大流情况下的最小费用. 输入格式 第一行包含四个正整数N ...
- Boxes Packing
Boxes Packing Mishka has got n empty boxes. For every i (1 ≤ i ≤ n), i-th box is a cube with side le ...
- PTA数据结构与算法题目集(中文) 7-43字符串关键字的散列映射 (25 分)
PTA数据结构与算法题目集(中文) 7-43字符串关键字的散列映射 (25 分) 7-43 字符串关键字的散列映射 (25 分) 给定一系列由大写英文字母组成的字符串关键字和素数P,用移位法定义 ...
- shell查询目标jvm的perm占比
#查询指定进程号下面的方法区使用率,jdk1.7是perm,jdk1.8是metaspace function get_perm_use_percent() { pid="$1" ...
- java中OOM错误解析(面试可以聊的东西)
嗯,生活加油鸭.... 实习中遇到OOM错误 GC overhead limit exceeded 问题,所以整理一下OOM异常问题: 先看一下“阿里的开发手册”对OOM的描述: OOM,全称“Out ...