集成Apple登录
- 客户端使用idToken(JWT)给到服务器,服务器使用苹果公钥解开JWT
identityToken参考样例:
// jwt 格式 该token的有效期是10分钟(通过.隔开,第一段header,第二段payload,第三段signature)
eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuZGV2aWNlbW9uaXRvciIsImV4cCI6MTU2NTY2ODA4NiwiaWF0IjoxNTY1NjY3NDg2LCJzdWIiOiIwMDEyNDcuOTNiM2E3OTlhN2M4NGMwY2I0NmNkMDhmMTAwNzk3ZjIuMDcwNCIsImNfaGFzaCI6Ik9oMmFtOWVNTldWWTNkcTVKbUNsYmciLCJhdXRoX3RpbWUiOjE1NjU2Njc0ODZ9.e-pdwK4iKWErr_Gcpkzo8JNi_MWh7OMnA15FvyOXQxTx0GsXzFT3qE3DmXqAar96nx3EqsHI1Qgquqt2ogyj-lLijK_46ifckdqPjncTEGzVWkNTX8uhY7M867B6aUnmR7u-cf2HsmhXrvgsJLGp2TzCI3oTp-kskBOeCPMyTxzNURuYe8zabBlUy6FDNIPeZwZXZqU0Fr3riv2k1NkGx5MqFdUq3z5mNfmWbIAuU64Z3yKhaqwGd2tey1Xxs4hHa786OeYFF3n7G5h-4kQ4lf163G6I5BU0etCRSYVKqjq-OL-8z8dHNqvTJtAYanB3OHNWCHevJFHJ2nWOTT3sbw
// header 解码
{"kid":"AIDOPK1","alg":"RS256"} 其中kid对应上文说的密钥id
// claims 解码
{
"iss":"https://appleid.apple.com", // 苹果签发的标识
"aud":"com.skyming.devicemonitor", // 接收者的APP ID
"exp":1565668086,"iat":1565667486,
"sub":"001247.93b3a799a7c84c0cb46cd08f100797f2.0704", //用户的唯一标识
"c_hash":"Oh2am9eMNWVY3dq5JmClbg",
"auth_time":1565667486
}
其中 iss标识是苹果签发的,aud是接收者的APP ID,该token的有效期是10分钟,sub就是用户的唯一标识
# 苹果api
# 公钥
https://appleid.apple.com/auth/keys
# 生成idToken(需要添加浏览器UA, 否则invalid_client)
https://appleid.apple.com/auth/token
验证demo参见cert目录
响应内容:
# 公钥
{
"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"
}
]
}
服务端从上面的数组中的公钥验证客户端的idToken
详细步骤
idToken方式登录(适用于客户端)
1. 创建App ID,添加Sign in with Apple
支持
2. 客户端获取idToken请求服务端
3. 服务端获取idToken进行验证(node版本)
const NodeRSA = require('node-rsa');
const axios = require('axios');
const jwt = require('jsonwebtoken');
const express = require('express')
const app = express()
/*
alg:string
The encryption algorithm used to encrypt the token.
e: string
The exponent value for the RSA public key.
kid: string
A 10-character identifier key, obtained from your developer account.
kty: string
The key type parameter setting. This must be set to "RSA".
n: string
The modulus value for the RSA public key.
use: string
* kid,为密钥id标识,签名算法采用的是RS256(RSA 256 + SHA 256),kty常量标识使用RSA签名算法,其公钥参数
*/
// 获取苹果的公钥
async function getApplePublicKey() {
let res = await axios.request({
method: "GET",
url: "https://appleid.apple.com/auth/keys",
headers: {
'Content-Type': 'application/json'
}
})
let keyvalues = {};
console.log(res.data)
// 循环验证
res.data.keys.map((key) => {
// parse key
const rsa = new NodeRSA();
rsa.importKey({
n: Buffer.from(key.n, 'base64'),
e: Buffer.from(key.e, 'base64')
}, 'components-public');
const publicKey = rsa.exportKey(['public']);
// cache key
keyvalues[key.kid] = publicKey;
// return public key string
return publicKey;
});
return keyvalues;
};
// 验证id_token
// id_token: Identity token
// audience : app bundle id , 可以不用
// subject : userId , 可以不用
async function verifyIdToken(id_token, audience, subject, callback) {
let body = Buffer.from(id_token.split('.')[0], 'base64').toString()
let json = JSON.parse(body)
console.log(json)
const map = await getApplePublicKey();
let publicKey = map[json.kid];
jwt.verify(id_token, publicKey, { algorithms: 'RS256', issuer: "https://appleid.apple.com", audience, subject }, (err, decode) => {
if (err) {
//message: invalid signature / jwt expired
console.log("JJ: verifyIdToken -> error", err.name, err.message, err.date);
callback && callback(err);
} else if (decode) {
// let decode = {
// iss: 'https://appleid.apple.com',
// aud: 'xxxxxxxx',
// exp: 1579171507,
// iat: 1579170907,
// sub: 'xxxxxxxx.xxxx',
// c_hash: 'xxxxxxxxxxxx',
// email: 'xxxxx@qq.com',
// email_verified: 'true',
// auth_time: 1579170907
// }
console.log("JJ: verifyIdToken -> decode", decode)
callback && callback(decode);
// sub 就是用户的唯一标识, 服务器可以保存它用来检查用户有没用过apple pay login , 至于用户第一次Login时,服务器就默认开一个member 给用户, 还是见到没login 过就自己再通过app 返回到注册页面再接着注册流程, 最后再pass userId 到server 保存. 这个看公司需求.
}
});
};
app.get('/', async function (req, res) {
// 客户端请求服务器的idToken
let idtoken = 'eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY24ucXVpY2tjbi5hcHAiLCJleHAiOjE1OTQ5NTY5MzAsImlhdCI6MTU5NDk1NjMzMCwic3ViIjoiMDAxNjY1LmIwMWUyMTdmYzEzNDRmMTdhYjM2NTM5NmMxN2Y1ZTFiLjA3NTEiLCJhdF9oYXNoIjoiMldDUUhVQVNQSm56QTJWWTRJczM1QSIsImVtYWlsIjoiaWQ0OW5yczd0Y0Bwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU5NDk1NjI4Miwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.KQNURkgdOxdf944lTly2fnvivIdSgeBf97IQkMSzGIa-PolPNHI4L-7jHveEm_qXE4ABRDczSSB_gpW7w4OCbHz7ewbF7C-YPE2siFvmasmHxVRyaRH_HUuRlU8KLXcOs2gLMLL-bXhRJGQUaFm41sGIxsxbY69JbHI_18Qz8S2AOR3F45XaxYjmEwpmT8YD05DILvI6DqCA9hwBT2YsLDq68ZvXLxdYI66EJC7n8dHqWGRQzrU53Z5DNYv_mZFWwwl04p0VgbbxQbhFzEV9BCDFoL7ytVG_NFiyOU1_GO69u-2p3Mt0EvDnlYhXkvZr5wqdpN53AI9GwegbqCieBw'
let data = await verifyIdToken(idtoken, '填入bundleId', null);
res.send(data);
})
app.listen(3000)
4. 响应客户端验证结果,成功即绑定用户信息
authorization_code方式登录(适用于网页端)
1. 创建App ID


2. 勾选Sign in with Apple
功能

3. 创建Services ID


注意:其中需要填入的Identifier
是接下来要使用的client_id
4. 配置用于验证数据的域名和回调地址


5. 获取自己应用的key_id
和key_file

6. 本地创建client_secret.rb文件
# ruby版本
# gem install jwt
require 'jwt'
# key_file = 'key.txt'
key_file = 'AuthKey_54N6776F2C.p8' # apple登录创建的key
key_id = '填入keyId' # apple登录创建的key的id
team_id = '填入teamId'
client_id = '填入bundleId' # 即app_id
ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file
headers = {
'alg' => 'ES256',
'kid' => key_id
}
claims = {
'iss' => team_id,
'iat' => Time.now.to_i,
'exp' => Time.now.to_i + 86400*180,
'aud' => 'https://appleid.apple.com',
'sub' => client_id,
}
token = JWT.encode claims, ecdsa_key, 'ES256', headers
puts token
执行命令,得到client_secret
su@Mac ~> ruby client_secret.rb
eyJhbGciOiJFUzI1NiIsImtpZCI6IjU0TjY3NzZGMkMifQ.eyJpc3MiOiJSNUY5MzJDNTNEIiwiaWF0IjoxNjAwMjIyNTA1LCJleHAiOjE2MTU3NzQ1MDUsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJjbi5xdWlja2NuLmFwcCJ9.ujJNrutWSjwkJGYQrzPhywkrdWo93bnT6rOiiPrPJYcnOvHPhuR0H_EoAV7Rj-p1iLg8QFzWPWO4sPJt0V5X6w
7. 网页端通过上面获取的client_secret进行登录
<?php
session_start();
$client_id = '填入client_id';
// 上面获取的client_secret
$client_secret = 'eyJhbGciOiJFUzI1NiIsImtpZCI6IjU0TjY3NzZGMkMifQ.eyJpc3MiOiJSNUY5MzJDNTNEIiwiaWF0IjoxNjAwMjI3NDUwLCJleHAiOjE2MTU3Nzk0NTAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJjbi5xdWlja2NuLmFwcCJ9.hdi5m83Z2sPPykjUREw9uNqDwgcjjplHtndUPpGPepNjKpABw_U3TOzvR2KRc3r_GYw_svKR5JEFcJOPcGZflg';
// 填入自己配置的url, 可先用下面地址测试流程
$redirect_uri = 'https://example-app.com/redirect';
///////////////////////////////////////////////////////////////////////
// 处理用户Apple登录成功后的回调
if(isset($_POST['code'])) {
if($_SESSION['state'] != $_POST['state']) {
die('Authorization server returned an invalid state parameter');
}
// Token endpoint docs:
// https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens
$response = http('https://appleid.apple.com/auth/token', [
'grant_type' => 'authorization_code',
'code' => $_POST['code'],
'redirect_uri' => $redirect_uri,
'client_id' => $client_id,
'client_secret' => $client_secret,
]);
if(!isset($response->access_token)) {
echo '<p>Error getting an access token:</p>';
echo '<pre>'; print_r($response); echo '</pre>';
echo '<p><a href="/">Start Over</a></p>';
die();
}
echo '<h3>Access Token Response</h3>';
echo '<pre>'; print_r($response); echo '</pre>';
$claims = explode('.', $response->id_token)[1];
$claims = json_decode(base64_decode($claims));
echo '<h3>Parsed ID Token</h3>';
echo '<pre>';
print_r($claims);
echo '</pre>';
die();
}
///////////////////////////////////////////////////////////////////////
$_SESSION['state'] = bin2hex(random_bytes(5));
$authorize_url = 'https://appleid.apple.com/auth/authorize'.'?'.http_build_query([
'response_type' => 'code',
'response_mode' => 'form_post',
'client_id' => $client_id,
'redirect_uri' => $redirect_uri,
'state' => $_SESSION['state'],
'scope' => 'name email',
]);
echo '<a href="'.$authorize_url.'">Sign In with Apple</a>';
8. 待用户登录后,将会重定向到redirect_uri
响应信息如下:
Form Data:
state: d4523690c6
code: c4d37a0e3c21947dca3668bea92da6adb.0.rrwwv.jk58r6H_MbDYRJhmb6F-UQ
9. 后续尚未验证过
登录url:
https://appleid.apple.com/auth/authorize?response_type=code&response_mode=form_post&client_id=$client_id&redirect_uri=$redirect_url&state=$state&scope=name+email
范例url:
https://appleid.apple.com/auth/authorize?response_type=code&response_mode=form_post&client_id=com.example.appsign&redirect_uri=https%3A%2F%2Fexample-app.com%2Fapi%2Fapple%2FoauthNotify&state=66232e40d8&scope=name+email
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!