最近有新产品要提交App Store,发现APP审核增加了接入apple帐号登录的要求。所以,借此机会研究下apple帐号登录,做一个分享。
Sign In With Apple
这是苹果推出一套标准接口,用户通过端上的Apple ID登录第三方APP。官方网址 https://developer.apple.com/documentation/sign_in_with_apple
时序图如下:
接入过程分两步:
1、客户端拉起用户登陆,获取授权码及用户信息
2、服务端验证
客户端拉起apple授权页面,等用户授权登录后,可以取到用户的 authorizationCode 、 identityToken 、user 等。
客户端的请求结果如下:
#pragma mark - ASAuthorizationControllerDelegate - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) { if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) { ASAuthorizationAppleIDCredential *credential = authorization.credential; NSString *state = credential.state; NSString *user = credential.user; NSPersonNameComponents *fullName = credential.fullName; NSString *email = credential.email; NSString *authorizationCode = [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding]; NSString *identityToken = [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding]; ASUserDetectionStatus realUserStatus = credential.realUserStatus; NSLog(@"user: %@", user); NSLog(@"fullName: %@", fullName); NSLog(@"email: %@", email); NSLog(@"authorizationCode: %@", authorizationCode); NSLog(@"identityToken: %@", identityToken); NSLog(@"realUserStatus: %@", @(realUserStatus)); } }
这些信息具体含义和用途:
∙ 用户ID: user,苹果用户唯一标识,该值在同一个开发者账号下的所有 App 下是一样的。
∙ 验证数据: identityToken , authorizationCode ,用于服务端验证授权请求的合法性。
∙ 用户信息: fullName , email,包括全名、邮箱等。
∙ 真实用户标志: realUserStatus ,用于判断当前账号是否是一个真实用户,取值有:unsupported、unknown、likelyReal。
其实,根据 identityToken ,解析后可以得到用户信息。但是,数据从客户端取到,然后转发给服务端,这个过程中,数据存在被修改的可能性。
有两种验证用户的方法:
1、 解析出用户数据,再利用 Apple 公钥验证 identityToken
2、 根据 authorizationCode 从 Apple ID server 重新拿到 identityToken, 再解析出用户数据
当然,方法2拿到的 identityToken 也可以用公钥验证
解析验证 identity Token
从网上找了一个苹果服务器返回的 identityToken ,数据如下:
eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmNoYW5nZGFvLnR0c2Nob29sIiwiZXhwIjoxNTg5MTg1Mjg1LCJpYXQiOjE1ODkxODQ2ODUsInN1YiI6IjAwMTk0MC43YTExNDFhYTAwMWM0NjllYTE1NjNjNmJhZTk5YzM3ZC4wMzA3IiwiY19oYXNoIjoiN1gzc2x2dHVBU0kwYmFSbU0wVGFrQSIsImVtYWlsIjoiYXEzMmsydnpjd0Bwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU4OTE4NDY4NSwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.S9wCOt6EeOoRrSMq4kUkPgJPyP1ruMXEcEZeeQEd1CDpcyVWLI8nTOqrl-l0sWYR-5nl2-1iJyiu77fRv8T7dBoV0EHT7GgM1l7qhnWsI9I8V-56rA9ArdJrLIBJbxu7j-xzQhZb6PZ5MSxPZ6WqZay0RpP9JiQ23ybssWQsMnqzvVZkye0iNtBGT1LnfT80XNxmj8L2uJZY08mXjjWWsYY_h0_IRvqOLyaW99w-F8T9KuDkWz2Z-DJX_tiKC0DOT03ypBv82H0v_v-8lFlp4rNRSB82CdgfYwEWElU7zKZfaHJOxT3wOvRXNpbj6_hENPdbtG2ozgdg2oVEiamz0g |
这是一个 JWT 数据串,先说下公钥验证的方法。
首先,要取到苹果的公钥,公钥目前是以 JWK 的形式对外发布,需要自己转成pem,获取的URL为 https://appleid.apple.com/auth/keys,数据如下:
{ "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" } ] }
实际上,这里是两个JWK数据,我们需要转成对应的证书pem文件。
字段的含义如下:
alg | The encryption algorithm used to encrypt the token. |
e | The exponent value for the RSA public key. |
kid | A 10-character identifier key, obtained from your developer account. |
kty | The key type parameter setting. This must be set to "RSA". |
n | The modulus value for the RSA public key. |
use | The intended use for the public key. |
其中,e、n以及kid使用了大端字节序表示,再通过base64url编码
这里,我封装了JWK转pem的方法(目前只支持 RSA公钥,以后看需要拓展。)
RSA公钥需要 n、e字段,接口如下:
local jwt = require "jwt" local ok, pem = jwt.jwk_to_pem(modulus, exponent) print(">> jwt.jwk_to_pem(modulus, exponent)") print(pem)
执行结果如下:
>> jwt.jwk_to_pem(modulus, exponent) -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiGaLqP6y+SJCCBq5Hv6p GDbG/SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInq UvjJur++hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPyg jLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk+ILjv1bORSRl 8AK677+1T8isGfHKXGZ/ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl 4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw+zHL wQIDAQAB -----END PUBLIC KEY-----
然后,利用公钥 pem 验证 identityToken(利用第三方库libjwt实现),方法如下
local jwt = require "jwt" local ok, token_str = jwt.jwt_decode(jwt_str, pem) print(">> jwt.jwt_decode(jwt_str, pem)") print(token_str)
执行结果如下:
>> jwt.jwt_decode(jwt_str, pem) {"alg":"RS256","kid":"86D88Kf","typ":"JWT"}.{"aud":"com.xxx.xxx","auth_time":1589184685,"c_hash":"7X3slvtuASI0baRmM0TakA","email":"aq32k2vzcw@privaterelay.appleid.com","email_verified":"true","exp":1589185285,"iat":1589184685,"is_private_email":"true","iss":"https://appleid.apple.com","nonce_supported":true,"sub":"001940.7a1141aa001c469ea1563c6bae99c37d.0307"}
如果不想公钥验证,直接解析 identityToken的方法如下
local jwt = require "jwt" local ok, token_str = jwt.jwt_decode(jwt_str) print(">> jwt.jwt_decode(jwt_str)") print(token_str)
执行结果如下:
>> jwt.jwt_decode(jwt_str) {"alg":"none","kid":"86D88Kf"}.{"aud":"com.xxx.xxx","auth_time":1589184685,"c_hash":"7X3slvtuASI0baRmM0TakA","email":"aq32k2vzcw@privaterelay.appleid.com","email_verified":"true","exp":1589185285,"iat":1589184685,"is_private_email":"true","iss":"https://appleid.apple.com","nonce_supported":true,"sub":"001940.7a1141aa001c469ea1563c6bae99c37d.0307"}
说下这几个字段:
{ "alg":"none", "kid":"86D88Kf" } { "aud":"com.xxx.xxx", "auth_time":1589184685, "c_hash":"7X3slvtuASI0baRmM0TakA", "email":"aq32k2vzcw@privaterelay.appleid.com", "email_verified":"true", "exp":1589185285, "iat":1589184685, "is_private_email":"true", "iss":"https://appleid.apple.com", "nonce_supported":true, "sub":"001940.7a1141aa001c469ea1563c6bae99c37d.0307" }
aud为app id,也就是请求的client_id, sub为用户在该app下的唯一id, iat为token的创建时间,exp为token的有效期,email为邮箱地址,其他字段的含义可以看这里
另外,说下c_hash, at_hash, s_hash:
c_hash : code 的hash值
at_hash : access_token 的hash值
s_hash : state 的hash值
我没有做过验证,在apple官网没有找到答案。网上找到OAuth 2.0开放协议对这块有说明 https://openid.net/specs/openid-connect-core-1_0.html#TokenSubstitution
$code = "c37906543364e6b"; $hash_var = hash("sha256", $code); $first_16_bytes = substr($hash_var, 0, 16); $b64_var = base64_encode($first_16_bytes); echo rtrim(strtr($b64_var, '+/', '-_'), '=');
服务端获取 identity Token
利用 authorizationCode 获取 identityToken 的方法:(官方文档 https//developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)
验证的过程是HTTPS验证,API如下:
POST https://appleid.apple.com/auth/token
请求字段说明:
client_id:这个是app id
client_secret: 密钥信息,使用 JWT编码,详见后文
code: 授权码,传客户端取到的 authorizationCode
grant_type: 此时固定写 authorization_code
其他非必要字段,就不做说明。
其中,需要特别讲下 client_secret,是一个json数据,例子如下:
{ "alg": "ES256", "kid": "ABC123DEFG" } { "iss": "DEF123GHIJ", "iat": 1437179036, "exp": 1493298100, "aud": "https://appleid.apple.com", "sub": "com.mytest.app" }
其中,iss为team id,iat为当前时间戳,exp为失效时间戳,aud固定,sub为app包名; kid为开发者帐号后台申请private key时系统附带生成的key id,猜测是apple用以确定解密对应的public key
这个字段需要使用 JWT 编码,转成 JWT数据串。
这里,我封装了jwt,如下:
>> jwt.jwt_encode(header, payload, pri_pem, 'es256') eyJhbGciOiJFUzI1NiIsImtpZCI6ImFiY2RlZmciLCJ0eXAiOiJKV1QifQ.eyJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyLCJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIn0.QDzbIkVpLZ1Uf6OwnrabKnz9xH3WJ_nLoiUZlT37IiVu3aXEMCfZkE3LlDUo14JUE6iBHo1B_jG91zwZOz7oZA
JWT是一个基于JSON结构化的编码标准(RFC 7519),它定义了一种紧凑的、自包含的方式,可在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。(jwt.io可获取实现算法)
JWT由三部分组成,之间用圆点(.)连接。这三部分分别是:Header.Payload.Signature
Header | 头部域,使用Base64编码,会消除等号 | { "alg": "HS256", "typ": "JWT" } |
Payload | 数据域,使用 Base64编码,会消除等号 | { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } |
Signature | 签名,对Header.Payload进行签名,支持多种算法 |
优点:支持多种签名算法,可兼顾安全性和效率,避免数据伪造
缺点:数据都是使用base64_encode简单编码,不能传输敏感信息。
需要注意的是,验证接口 /auth/token 返回的数据,字段如下:
access_token | (Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access. |
expires_in | The amount of time, in seconds, before the access token expires. |
id_token | A JSON Web Token that contains the user’s identity information. |
refresh_token | The refresh token used to regenerate new access tokens. Store this token securely on your server. |
token_type | The type of access token. It will always be bearer. |
关键字段 id_token,就是前面提到的 identity Token, 即用户信息,同样使用jwt编码,参照上文解开。
最后语
文章到这里就结束了,最后分享文中提到的 lua jwt库, 地址 https://github.com/chenweiqi/lua_jwt
主要逻辑是c开发的,lua只是实现扩展,有遇到bug喊我!
踩踩,好久不见,大佬
谢谢支持!
大佬不用erlang了吗 多谢谢erlang的文章吧
现在项目没用erlang了,有机会我再整理下
有用,非常感谢