09b4250476222c118f8522f40192c0fa
WWDC20 10173 - 深入使用 “通过 Apple 登录”

WWDC 2020 Session 10173: Get the most out of Sign in with Apple

Sign in with Apple(“通过 Apple 登录”)在 WWDC 2019 随着 iOS 13 和 macOS 10.15 以及 Xcode 11 一起推出,在去年的小专栏 WWDC19 内参,我也分享了一篇文章,详细介绍了 app 如何接入 Sign in with Apple 能力:

Sign in with Apple 让用户能用自己的 Apple ID 轻松登录开发者的 app 和网站。用户不必填写表单、验证电子邮件地址和选择新密码,就可以使用“通过 Apple 登录”设置帐户并立即开始使用 app。所有帐户都通过双重认证受到保护,具有极高的安全性:

其中,最重要的是 Apple 提供了 Sign in with Apple JS SDK,使得它可以跨平台使用:

此外,苹果也在去年更新了《App Store 审核指南》,加入了 “4.8 通过 Apple 登录” 一条,要求所有使用第三方或社交登录服务的 app,都必须同时接入 Sign in with Apple 作为同等选项。目前,国内的大部分 app 也基本都集成了这个能力。

本文将带你先简单回顾一下 WWDC 2019 中介绍的如何快速集成 “苹果登录” 能力,然后解读 WWDC 2020 中 Sign in with Apple 的新增 API 和相关新特性:

  • Creating a secure request(更安全的授权请求)
  • Credential state changes(处理授权凭证状态变化)
  • Server to server notifications(服务端通知)
  • Sign in with Apple Button(支持 SwiftUI
  • Upgrading to Sign in with Apple(在现有的账号体系中快速集成苹果登录能力)

0. 集成 Sign in with Apple

在 app 中集成 “Sign in with Apple” 能力,大致需要以下 4 步骤:

(1)添加苹果登录按钮
(2)点击发起授权请求
(3)处理回调数据,并在服务端验证结果
(4)处理苹果账号会话发生变化

详细的集成说明和示例代码,请查看去年 WWDC19 内参的文章,这里不再赘述:

补充:上述文章在集成苹果登录需要做哪些配置,以及在客户端拿到 authorizationCode 和 identityToken 后传给服务端,服务端如何调苹果提供的 REST API 进行验证,没有比较详细的说明,可以参考如下两篇文章:

下面我们介绍一下 WWDC 2020 中 Sign in with Apple 的新内容。

1. 更安全的授权请求

发起授权

当我们点击 “Sign in with Apple” 按钮发起授权登录请求时,iOS 系统自带的 Apple ID 双重因子身份验证使得我们 app 的账户已经具备很好的安全能力,但我们仍然可以做一些事情,使得授权请求更加安全。

一般情况下,app 发起授权登录请求的代码(Swift)大致如下:

// Configure request, setup delegates and perform authorization request
@objc func handleAuthorizationButtonPress() {
    let request = ASAuthorizationAppleIDProvider().createRequest()
    request.requestedScopes = [.fullName, .email]

    request.nonce = myNonceString()
    request.state = myStateString()

    let controller = ASAuthorizationController(authorizationRequests: [request])

    controller.delegate = self
    controller.presentationContextProvider = self

    controller.performRequests()
}

这里有两个参数 noncestate,可以用于验证执行请求后获得的授权凭证是否符合预期。

  • nonce:发起授权请时,开发者设置给 request 的不透明数据块(opaque blob of data),它是一串字符串,用于唯一标识一次授权请求,每次创建新请求时都应设置不一样的值。该值会作为请求响应(ASAuthorization response)中 identityToken 的一个属性直接返回。因此服务端在验证 identityToken 时,可以同时验证此值,检查该 nonce 之前是否已经使用过,有助于防止重放攻击(replay attacks)

  • state:与 nonce 类似,由开发者手动设置给 request,也是一串字符串;它会直接在响应 response 中返回,使得开发者可以在本地将授权凭证结果与请求匹配,判断当前响应是否由本设备中开发者自己的 app 发起的。

注1:Replay Attacks,重放攻击,又称重播攻击、回放攻击,是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。

注2:identityToken 是一个 JWT 格式的加密数据,JWT 相关知识介绍详见:JSON Web Token

简单地说,nonce 和 state 都是开发者在发起授权时设置给 request 对象,在授权回调中,Apple 都会原封不动地返回,nonce 会拼接在 identityToken 中,主要在服务端验证,用于防止重放攻击;而 state 则是直接在授权响应结果中返回,用于客户端本地验证请求是否有当前 app 发起的。

设置完这两个参数,我们就可以发起授权登录请求了,当 request 的 scopes 中我们设置了 fullNameemail,用户会看的如下页面:

此处,用户可以选择 “共享电子邮件” 或者 “隐藏邮件地址”。当用户选择 “隐藏邮件地址” 时,开发者会拿到一个中转的专用邮件地址(eg: pfju4f59kj@privaterelay.appleid.com),开发者发送到该 email 的邮件会被自动转发给用户真实的邮箱上,用户也可以直接回复邮件给开发者。

因此,开发者可以直接把这个中转的邮件地址当成真实的 email 来使用,且对于同一个开发者账号下的所有 app,同一个用户 Apple ID 得到的 private relay email 地址是一样的。

验证结果

如上所述,当授权成功后,我们可以在 delegate 方法中得到回调结果,代码如下:

// ASAuthorizationControllerDelegate
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
        let userIdentifier = credential.user
        let fullName = credential.fullName
        let email = credential.email
        let realUserStatus = credential.realUserStatus

        let state = credential.state
        let identityToken = credential.identityToken
        let authorizationCode = credential.authorizationCode

        // Securely store the userIdentifier locally
        self.saveUserIdentifier(userIdentifier)

        // Create a session with your server and verify the information
        self.createSession(identityToken: identityToken, authorizationCode: authorizationCode)
    }
}

在回调结果中,我们可以取到用户的 email、fullName、realUserStatus、userIdentifier 等用户信息,同时也可以拿到 state、authorizationCode、identityToken(包含 nonce 字段) ,通过这些字段,我们可以安全地验证请求的合法性,并与服务端创建会话信息(session)。

此外,需要注意的一点是,由于 fullName、email 仅会在第一次授权 app 时才会包含在结果凭据中,后续的重新授权都将不会再包含这俩信息(即使请求 scopes 里设置了 .fullName.email,除非用户在设置了删除了对该 app 的授权后,再次触发才会重新返回这俩字段),因此,我们的 app 如果需要用到这两个字段,应该在第一次授权请求结果中把它们缓存起来,这样就不会丢失所需的重要信息。

如前所述,响应结果中拿到 state 可用于客户端本地验证该结果是否属于当前的请求。而另外两个参数 authorizationCode、identityToken 需要传给服务端进行验证和解码(其实就是简单的 Base64 解码),解码后的结果如下:

top Created with Sketch.