在iOS 13系统中,Apple要求提供第三方登录的APP也需要支持「Sign With Apple」。在WWDC 2019中,Apple详细讲解了「Sign In with Apple」的使用:
Introducing Sign In with Apple
这个视频主要演示了在APP端集成和使用「Sign In with Apple」,包含几个主要步骤:
- 创建UI
Apple要求在UI上显示的样式必须符合要求,使用ASAuthorizationAppleIDButton可以快速创建符合要求的Button。
- 触发登录请求
典型的示例代码:
let provider = ASAuthorizationAppleIDProvider()
let request = provider.createRequest()
request.requestedScopes = [.fullName, .email]//请求的用户信息
let vc = ASAuthorizationController.init(authorizationRequests: [request])
vc.delegate = self
vc.presentationContextProvider = self
vc.performRequests()
- 实现ASAuthorizationControllerPresentationContextProviding,ASAuthorizationControllerDelegate协议
以上步骤在上述视频中有详细讲解,在这里就不再展开。还有包括获取授权状态,从iCloud KeyChain password中快速获取登录信息等API也都比较简单,不再说明。这篇文章主要说明下服务端处理部分。
在APP中,「Sign In with Apple」操作最终会在ASAuthorizationControllerDelegate的:
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization)
方法中获取账号登录信息。如果认证成功会返回ASAuthorizationAppleIDCredential类型的对象,它的主要属性如下:
- user
用户唯一ID,在一个开发者账号下的APP获取到的是一样的,类似微信小程序API中的openid;
- identityToken
「JWT」格式的token,用于验证信息合法性。
- email,fullName:用户邮箱,昵称等信息;
- realUserStatus:是否是“真实用户”,可用于反作弊,对抗黑灰产;
那么拿到这些信息该如何使用?
你可以将user,email,fullName等信息直接传递给后端,后端建立对应的账号信息之后,返回给APP。但这样有个重要的问题就是不能保证安全性,无法判断请求是否是伪造的。这个时候就要使用identityToken了。
注意:当第一次认证成功之后,将不会再返回email,fullName等信息,可以在设置->Apple ID->密码与安全性->使用您AppleID的App 中删除对应的APP。
identityToken的使用
将identityToken转换为字符串:
let identityTokenString = String(data: identityToken!, encoding: .utf8)
identityTokenString实际上是JWT(JSON Web Token)格式的文件,JWT文件由三部分组成:
- Header
- Payload
- Signature
这三部分由"."分割,其中Header和Payload是经过base64编码的。
Header base64解码之后示例:
{
"alg": "HS256", //算法类型
"typ": "JWT" //token类型
}
Payload base64解码之后示例:
{
"iss": "https://appleid.apple.com",//数据签发者
"aud": "com.kanchuan",//签发对象
"exp": 1568090840,//过期时间
"iat": 1568090240,//签发时间
"sub": "022409.17avbbaf112941e5a722788e7f3880f4.4565",//用户唯一ID
"c_hash": "bck7ThP_-cuu0nbwWSQOPQ",
"auth_time": 1568090240
}
而Signature部分就是对Header及Payload两部分内容按指定算法进行签名,大致逻辑如下:
Signature = signature(base64UrlEncode(Header) + "." + base64UrlEncode(Payload), secretKey)
#signature代表具体的加密算法;
#secretKey为密钥;
具体到identityToken,Apple目前采用的是RS256的非对称加密算法:
- Apple会使用私钥(也即为上面的secretKey)对Header及Payload加密,获取Signature;
- 将Header,Payload及Signature信息包装为JWT格式文件,即是identityToken;
那么,我们如何才能验证拿到的identityToken是否合法呢,这就要用到Apple提供的公钥了。公钥获取地址:
https://appleid.apple.com/auth/keys
//返回的是一个JSON,包含公钥信息。
{
"keys": [
{
"kty": "RSA",
"kid": "AIDOPK1",
"use": "sig",
"alg": "RS256",
"n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
"e": "AQAB"
}
]
}
解析identityToken后,通过公钥解密Signature获得Payload信息,和identityToken中存储的Payload对比即可校验信息是否合法。
解析JWT文件和校验的过程已经有不少开源实现,参考JWT的官网:
提供了不同语言的开源实现,以PHP项目为例,我选用了:
使用它的RS256的接口:
use Firebase\JWT\JWT;
$jwt = "";//jwt字符串
$publicKey = "-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H
4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t
0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4
ehde/zUxo6UvS7UrBQIDAQAB
-----END PUBLIC KEY-----";//pem公钥
$decoded = JWT::decode($jwt, $publicKey, array('RS256'));
print_r($decoded);
注意到这里使用的是PEM格式的公钥,可以通过将RSA公钥modulus(N)和exponent(E)转换为PEM文件,参考:convert-rsa-public-key-to-pem-format
也可以通过下方页面在线将JWT格式公钥直接转为PEM文件:
跨平台支持
Apple文档的Generate and validate tokens,实际上并不是作用于APP的,而是针对WEB产品的。通过APP端的接口已经能获取token,过期时间等信息,只需要按照上述方式走校验逻辑即可。
「Sign With Apple」提供的JS版本API,能够跨平台支持「Sign With Apple」。WEB APP需要额外的配置,参考:Configure Sign In with Apple for the web
遗留的问题
虽然上述过程解决了服务端验证identityToken的问题,但有隐患:上述过程仅处理了RS256算法的情况,当苹果修改公钥算法之后,需要进行再适配。保险的做法是通过auth/keys接口返回数据自适应解析。
留言板