APPLE游客验证登录代码
代码1: lib.applelogin.php
<?php
/**
* apple登陆
*
* @author kevinxie
* @date 2020/04/30
*/
class Lib_Applelogin
{
protected static $_instance = array();
public function __construct()
{
}
/**
* 对像生产器
*
* @return object
*/
public static function factory()
{
if ( !isset( self::$_instance[__METHOD__] ) || !is_object( self::$_instance[__METHOD__] ) )
{
self::$_instance[__METHOD__] = new self();
}
return self::$_instance[__METHOD__];
}
/**
* Apple登陆验证
*
* @param $clientUser 验证的用户id
* @param $identityToken 验证token
* @return array
*/
public static function appleAuth( $clientUser, $identityToken)
{
//使用Apple标识令牌解析提供的登录,返回一个对象
$appleSignInPayload = Lib_Applelogin::factory()->getAppleSignInPayload($identityToken);
/**
* 确定客户端提供的用户是否有效
*/
$isValid = $appleSignInPayload->verifyUser($clientUser);
/**
* 使用Apple电子邮件和用户凭据获取登录
*/
$email = $appleSignInPayload->getEmail();
$userInfo['email'] = $email;
if ( $isValid)
{
$ret["errorCode"] = 0;
$ret["errorMsg"] = "success";
$ret["data"] = $userInfo;
}
else
{ //无效用户
$ret["errorCode"] = -1;
$ret["errorMsg"] = "fail";
$ret["data"] = '';
}
return $ret;
}
/**
* 使用Apple标识令牌解析提供的登录
*
* @param string $identityToken
* @return array
*/
public static function getAppleSignInPayload(string $identityToken)
{
$identityPayload = Lib_Applelogin::factory()->decodeIdentityToken($identityToken);
return Lib_Aspayload::factory($identityPayload);
}
/**
* 使用Apple的公钥对Apple编码的JWT进行解码以进行签名
*
* @param string $identityToken
* @return object
*/
public static function decodeIdentityToken(string $identityToken)
{
//解码token获取kid
$publicKeyKid = Lib_Jwt::factory()->getPublicKeyKid($identityToken);
$publicKeyData = Lib_Applelogin::factory()->fetchPublicKey($publicKeyKid);
$publicKey = $publicKeyData['publicKey'];
$alg = $publicKeyData['alg'];
$payload = Lib_Jwt::factory()->decode($identityToken, $publicKey, [$alg]);
return $payload;
}
/**
* 获取苹果的公钥,用于解码JWT中的登录。
*
* @param string $publicKeyKid
* @return array
*/
public static function fetchPublicKey(string $publicKeyKid)
{
//获取苹果公钥,有多个
$decodedPublicKeys = Lib_Functions::factory()->curl('https://appleid.apple.com/auth/keys');
if(!isset($decodedPublicKeys['keys']) || count($decodedPublicKeys['keys']) < 1)
{
die('Invalid key format.');
}
$kids = array_column($decodedPublicKeys['keys'], 'kid');
//获取对应的公钥
$parsedKeyData = $decodedPublicKeys['keys'][array_search($publicKeyKid, $kids)];
//创建证书,并从证书中解析出密钥
$parsedPublicKey= Lib_Jwt::factory()->parseKey($parsedKeyData);
//返回包含密钥详情的数组
$publicKeyDetails = openssl_pkey_get_details($parsedPublicKey);
if(!isset($publicKeyDetails['key']))
{
die('Invalid public key details.');
}
return [
'publicKey' => $publicKeyDetails['key'],
'alg' => $parsedKeyData['alg']
];
}
}
?>
代码2: lib.aspayload.php
<?php
/**
* 一个类装饰器,用于从客户端解码已签名的JWT
*/
class Lib_Aspayload
{
protected static $_instance = array();
protected $_instance2;
public function __construct( $instance)
{
if(is_null($instance))
{
die('ASPayload received null instance.');
}
$this->_instance2 = $instance;
}
/**
* 对像生产器
*
* @return object
*/
public static function factory( $param)
{
if ( !isset( self::$_instance[__METHOD__] ) || !is_object( self::$_instance[__METHOD__] ) )
{
self::$_instance[__METHOD__] = new self( $param);
}
return self::$_instance[__METHOD__];
}
public function __call($method, $args) {
return call_user_func_array(array($this->_instance2, $method), $args);
}
public function __get($key) {
return (isset($this->_instance2->$key)) ? $this->_instance2->$key : null;
}
public function __set($key, $val) {
return $this->_instance2->$key = $val;
}
public function getEmail() {
return (isset($this->_instance2->email)) ? $this->_instance2->email : null;
}
public function getUser() {
return (isset($this->_instance2->sub)) ? $this->_instance2->sub : null;
}
public function verifyUser(string $user) {
return $user === $this->getUser();
}
}
?>
代码3: lib.jwt.php
<?php
/**
* 基于此规范的JSON Web令牌实现
* https://tools.ietf.org/html/rfc7519
*
*/
class Lib_Jwt
{
/**
* The server leeway time in seconds, to aware the acceptable different time between clocks
* of token issued server and relying parties.
* When checking nbf, iat or expiration times, we want to provide some extra leeway time to
* account for clock skew.
*/
public static $leeway = 0;
/**
* Allow the current timestamp to be specified.
* Useful for fixing a value within unit testing.
*
* Will default to PHP time() value if null.
*/
public static $timestamp = null;
public 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'),
);
protected static $_instance = array();
public function __construct()
{
}
/**
* 对像生产器
*
* @return object
*/
public static function factory()
{
if ( !isset( self::$_instance[__METHOD__] ) || !is_object( self::$_instance[__METHOD__] ) )
{
self::$_instance[__METHOD__] = new self();
}
return self::$_instance[__METHOD__];
}
/**
* 将JWT字符串解码为PHP对象
*
* @param string $jwt The JWT
* @param string|array $key The key, or map of keys.
* If the algorithm used is asymmetric, this is the public key
* @param array $allowed_algs List of supported verification algorithms
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
*
* @return object The JWT's payload as a PHP object
*
* @uses jsonDecode
* @uses urlsafeB64Decode
*/
public static function decode($jwt, $key, array $allowed_algs = array())
{
$timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
if (empty($key)) {
die('Key may not be empty');
}
$tks = explode('.', $jwt);
if (count($tks) != 3) {
die('Wrong number of segments');
}
list($headb64, $bodyb64, $cryptob64) = $tks;
if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
die('Invalid header encoding');
}
if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
die('Invalid claims encoding');
}
if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
die('Invalid signature encoding');
}
if (empty($header->alg)) {
die('Empty algorithm');
}
if (empty(static::$supported_algs[$header->alg])) {
die('Algorithm not supported');
}
if (!in_array($header->alg, $allowed_algs)) {
die('Algorithm not allowed');
}
if (is_array($key) || $key instanceof \ArrayAccess) {
if (isset($header->kid)) {
if (!isset($key[$header->kid])) {
die('"kid" invalid, unable to lookup correct key');
}
$key = $key[$header->kid];
} else {
die('"kid" empty, unable to lookup correct key');
}
}
// 检查签名
if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
die('Signature verification failed');
}
// 检查是否定义了nbf。这是令牌可以是实际使用的时间。如果还没到那个时候,就中止
if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
die('Cannot handle token prior');
}
// 检查此令牌是否已在“now”之前创建。这将防止使用为以后使用而创建的令牌(并且没有正确使用nbf声明).
if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
die('Cannot handle token prior');
}
// 检查此令牌是否已过期.
// if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
// die('Expired token');
//}
return $payload;
}
/**
* URL安全Base64解码字符串,并转换成PHP对象
* @param string $jwt
* @return mixed
*/
public static function getPublicKeyKid(string $jwt)
{
$tks = explode('.', $jwt);
if (count($tks) != 3) {
die('Wrong number of segments');
}
list($headb64, $bodyb64, $cryptob64) = $tks;
if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
die('Invalid header encoding');
}
return $header->kid;
}
/**
* 将PHP对象或数组转换并签名为JWT字符串。
*
* @param object|array $payload PHP object or array
* @param string $key The secret key.
* If the algorithm used is asymmetric, this is the private key
* @param string $alg The signing algorithm.
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
* @param mixed $keyId
* @param array $head An array with header elements to attach
*
* @return string A signed JWT
*
* @uses jsonEncode
* @uses urlsafeB64Encode
*/
public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
{
$header = array('typ' => 'JWT', 'alg' => $alg);
if ($keyId !== null) {
$header['kid'] = $keyId;
}
if ( isset($head) && is_array($head) ) {
$header = array_merge($head, $header);
}
$segments = array();
$segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
$segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
$signing_input = implode('.', $segments);
$signature = static::sign($signing_input, $key, $alg);
$segments[] = static::urlsafeB64Encode($signature);
return implode('.', $segments);
}
/**
* 用给定的密钥和算法对字符串签名.
*
* @param string $msg The message to sign
* @param string|resource $key The secret key
* @param string $alg The signing algorithm.
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
*
* @return string An encrypted message
*/
public static function sign($msg, $key, $alg = 'HS256')
{
if (empty(static::$supported_algs[$alg])) {
die('Algorithm not supported');
}
list($function, $algorithm) = static::$supported_algs[$alg];
switch($function) {
case 'hash_hmac':
return hash_hmac($algorithm, $msg, $key, true);
case 'openssl':
$signature = '';
$success = openssl_sign($msg, $signature, $key, $algorithm);
if (!$success) {
die("OpenSSL unable to sign data");
} else {
return $signature;
}
}
}
/**
* 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
*/
private static function verify($msg, $signature, $key, $alg)
{
if (empty(static::$supported_algs[$alg])) {
die('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.
die(
'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);
}
}
/**
* 将JSON字符串解码为PHP对象。
*
* @param string $input JSON string
*
* @return object Object representation of JSON string
*
* @throws DomainException Provided string was invalid JSON
*/
public static function jsonDecode($input)
{
if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
/** 在PHP>=5.4.0中,json_decode()接受一个options参数,该参数允许您指定将大型int(如Steam事务id)视为字符串,而不是将它们转换为浮点数的PHP默认行为
*/
$obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
} else {
/**并非所有服务器都支持这一点,因此对于较旧的版本,我们必须在解码之前手动检测JSON字符串中的大整数并引用它们(从而将它们转换为字符串),因此preg_replace()调用.
*/
$max_int_length = strlen((string) PHP_INT_MAX) - 1;
$json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
$obj = json_decode($json_without_bigints);
}
if (function_exists('json_last_error') && $errno = json_last_error()) {
static::handleJsonError($errno);
} elseif ($obj === null && $input !== 'null') {
die('Null result with non-null input');
}
return $obj;
}
/**
* 将PHP对象编码为JSON字符串.
*
* @param object|array $input A PHP object or array
*
* @return string JSON representation of the PHP object or array
*
* @throws DomainException Provided object could not be encoded to valid JSON
*/
public static function jsonEncode($input)
{
$json = json_encode($input);
if (function_exists('json_last_error') && $errno = json_last_error()) {
static::handleJsonError($errno);
} elseif ($json === 'null' && $input !== null) {
die('Null result with non-null input');
}
return $json;
}
/**
* 使用URL安全Base64解码字符串.
*
* @param string $input A Base64 encoded string
*
* @return string A decoded string
*/
public static function urlsafeB64Decode($input)
{
$remainder = strlen($input) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$input .= str_repeat('=', $padlen);
}
return base64_decode(strtr($input, '-_', '+/'));
}
/**
* 使用URL安全Base64编码字符串
*
* @param string $input The string you want encoded
*
* @return string The base64 encode of what you passed in
*/
public static function urlsafeB64Encode($input)
{
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}
/**
* 创建JSON错误的Helper方法.
*
* @param int $errno An error number from json_last_error()
*
* @return void
*/
private static function handleJsonError($errno)
{
$messages = array(
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
);
die(
isset($messages[$errno])
? $messages[$errno]
: 'Unknown JSON error: ' . $errno
);
}
/**
* 获取加密字符串中的字节数.
*
* @param string
*
* @return int
*/
private static function safeStrlen($str)
{
if (function_exists('mb_strlen')) {
return mb_strlen($str, '8bit');
}
return strlen($str);
}
/**
* 分析一组JWK键
* @param $source
* @return array an associative array represents the set of keys
*/
public static function parseKeySet($source)
{
$keys = [];
if (is_string($source)) {
$source = json_decode($source, true);
} else if (is_object($source)) {
if (property_exists($source, 'keys'))
$source = (array)$source;
else
$source = [$source];
}
if (is_array($source)) {
if (isset($source['keys']))
$source = $source['keys'];
foreach ($source as $k => $v) {
if (!is_string($k)) {
if (is_array($v) && isset($v['kid']))
$k = $v['kid'];
elseif (is_object($v) && property_exists($v, 'kid'))
$k = $v->{'kid'};
}
try {
$v = self::parseKey($v);
$keys[$k] = $v;
} catch (Exception $e) {
}
}
}
if (0 < count($keys)) {
return $keys;
}
die('Failed to parse JWK');
}
/**
* 解析JWK键
* @param $source
* @return resource|array an associative array represents the key
*/
public static function parseKey($source)
{
if (!is_array($source))
$source = (array)$source;
if (!empty($source) && isset($source['kty']) && isset($source['n']) && isset($source['e'])) {
switch ($source['kty']) {
case 'RSA':
if (array_key_exists('d', $source))
die('Failed to parse JWK: RSA private key is not supported');
//创建证书
$pem = self::createPemFromModulusAndExponent($source['n'], $source['e']);
//从证书中提取公钥
$pKey = openssl_pkey_get_public($pem);
if ($pKey !== false)
return $pKey;
break;
default:
//Currently only RSA is supported
break;
}
}
die('Failed to parse JWK');
}
/**
*
* 从RSA模和指数信息创建以PEM格式表示的公钥
*
* @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
*/
private static function createPemFromModulusAndExponent($n, $e)
{
$modulus = self::urlsafeB64Decode($n);
$publicExponent = self::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;
}
/**
* 对长度进行编码
*
* 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
*/
private static function encodeLength($length)
{
if ($length <= 0x7F) {
return chr($length);
}
$temp = ltrim(pack('N', $length), chr(0));
return pack('Ca*', 0x80 | strlen($temp), $temp);
}
}
执行代码:
include lib.applelogin.php;
include lib.aspayload.php;
include lib.jwt.php;
//$clientUser 验证的用户id
//$identityToken 验证token</p>
$info = Lib_Applelogin::factory()->appleAuth( $clientUser, $identityToken);