iOS Sign With Apple实践

在iOS 13系统中,Apple要求提供第三方登录的APP也需要支持「Sign With Apple」,本文主要介绍「Sign With Apple」在服务端的校验逻辑。

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.easeapi.www",//签发对象
  "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的官网:

jwt.io

提供了不同语言的开源实现,以PHP项目为例,我选用了:

firebase/php-jwt

使用它的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文件:

JWK to PEM Convertor online

跨平台支持

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接口返回数据自适应解析。

其它文章

iOS 13 适配
iOS crash log分析实践
Address Sanitizer的原理和使用
iOS 13中dyld 3的改进和优化
iOS 13 Scene Delegate and multiple windows