在 iOS App 中支持 Sign In with Apple

前几天,苹果的开发者新闻Apple Developer News上,连发了4文。内容大致是,更新了App Store审核指导,要求在4月30日之前,iPhone、iPad应用必须启用Launch Screen storyboard,并且要支持所有尺寸的屏幕,上传App必须使用iOS13 SDK打包,还有就是要支持Sign In with Apple。所以,之前欠下的债,比如iPad没有适配iPad Pro等等,都要还一还债了。

还有就是,这两天的有消息称苹果要求在4月30日之前,App必须支持dark mode(暗黑模式),这个可能是个假消息。Apple只是说希望大家使用iOS13提供的新特性包括dark mode,但并没有强制要求。估计是哪个消息二道贩子误传或者为了流量无节操了。建议大家还是直接上苹果官网查看一手消息来的更准确一点。

相比于其他必须支持的项目来说,Sign In with Apple(姑且翻译为苹果登录吧)是一个相对比较陌生的概念。去年的时候,苹果文档中写到,如果App只有第三方登录方式,那么必须支持Sign In with Apple。最近又修改了,把“只(exclusively)”字去掉了,变成了有第三方登录的都需要支持了。所以到了最后4月30日最后期限,是否会严格执行这个条款,就看苹果高不高兴了,不高兴你的应用就栽了。

所以有备无患。不过也有几个例外的情况,可以不支持。所以大家可以先看例外部分。如果贵App属于例外部分,那就可以溜了🏃。

下面是我通过对Sign In with Apple两天的了解,总结了一下,就算抛砖引玉吧。

简单介绍及理解

因为苹果设备的用户,肯定会有一个苹果账号。苹果为了方便用户使用App,推出了Sign In with Apple,使用苹果账号登录各种App。Sign In with Apple与其他登录方式相比的有其优点,比如苹果一直标榜的信息安全,隐私保护,防止账户或者隐私泄露;还有就是一个账号在苹果任何平台下(iOS,iPadOS,Web,WatchOS等)通用;苹果不会追踪App的用户信息等。

从技术的角度看,Sign In with Apple可以理解为类似于微信或微博这样的第三方登录,通过OAuth授权机制,将苹果用户的用户信息与App进行共享。这样,App只要支持了Sign In with Apple,用户可以轻松的完成登录操作,提高登录率(?有待验证)。

那么就进入正题,先说一说App如何支持Sign In with Apple。

App支持Sign In with Apple

和Push通知类似,App如果需要支持Sign In with Apple,需要在苹果开发者后台先进行一些操作。

开发者后台操作

  1. 在开发者后台的Certificates, Identifiers & Profiles,找到左边的Identifiers,在右侧所有的App Id中,找到你需要支持Sign In with Apple的App Id,点击进入。

  2. 在这个 Edit your App ID Configuration 页面中,选中Sign In with Apple, 点击后面的Edit。Edit

  3. 在Edit页面,有两个选项,Enable as primary App ID or Group with an existing primary App ID。如果你是第一次为你的账号下的App支持Sign In with Apple,那么只能选择Enable as primary App ID,Group with an existing primary App ID选项是置灰不可选择的。操作完毕记得点击save保存。Enable这两个选项究竟什么意思呢?意思是说,如果你想独立为App开通Sign In with Apple,那么就选择Enable as primary App ID;如果你已经为某个App开通的Sign In with Apple,而另一个App想与之前开通的App实现用户信息通用,苹果定义为Group,就可以选择Group with an existing primary App ID。举个例子,如果某个Mac App已经支持了Sign In with Apple,后面又研发出App的iOS版本,那么该iOS版本就可以选择Group with an existing primary App ID选项,来与Mac App实现Group。当然,实现Group的App们也可以Ungroup,Ungroup也是在这里操作。

  4. 配置完App的Sign In with Apple,接下来回到Certificates, Identifiers & Profiles页面,点击左面的Keys,点击+加号,添加一组私钥。

  5. 在私钥添加页面,填写秘钥名称,选中Sign In with Apple,点击后面的Configure。Keys

  6. 选择此私钥关联的App,在下拉框中发现了之前支持Sign In with Apple的App Id,选择后保存即可。KeyConfigure

  7. 上面这些操作之后,App已经支持了Sign In with Apple。但是由于App的Capabilities有改动,所以Provisioning Profile需要重新生成。在Profiles里面找到需要重新生成的provisioning Profile,修改保存即可。点击下载为之后使用。

通过以上的步骤,开发者后台的操作已经完毕。

Xcode工程配置

除了上述的过程,Xcode同样需要做相关的配置。

  1. Xcode打开工程,选中Target,在Signing & Capabilities选项卡下,点击 + capability,选择Sign In with Apple,添加。capability

  2. 添加新下载的Provisioning File,在Build Settings选项卡下Provisioning Profile选择新Profile即可。

代码编写

苹果官方提供了一个示例代码。感兴趣的可以下载来看。

这里我就介绍一下主要用到的类

  1. ASAuthorizationAppleIDProvider
    从字面意思上看,是一个AppleID授权的提供者,意思是可以提供一个基于AppleID的授权请求机制。从API来看也确实如此,它可以创建一个Request,来请求用户授权。因为授权基于OAuth,如果大家了解一些OAuth授权的话,应该知道授权是有范围(scope)的概念的。比如用户姓名、邮箱等普通scope,或者电话等敏感scope,都是scope。用户可以根据你request的scope来决定是否授权,只有用户授权了才可以得到scope对应的信息。目前,苹果只提供两个用户信息,即fullName和email。示例代码:

    1
    2
    3
    let provider = ASAuthorizationAppleIdProvider()
    let request = provider.createRequest()
    request.requestedScopes = [.fullName, .email]
  2. ASAuthorizationController
    此类可以理解为视图控制器,虽然他不是UIViewController的子类。他可以接收上面的Request,从而提供Request对应授权页面。
    因为要弹出授权页面,所以需要提供给他弹出页面的上下文(presentationContextProvider),即从哪里弹出。所以必须将ASAuthorizationControllerpresentationContextProvider属性指向可以提供页面上下文的实例,然后此实例实现名为ASAuthorizationControllerPresentationContextProviding的protocol的方法。
    当然,用户授权页弹出之后,进行授权,我们也要知道用户是否授权、授权之后要得到授权后的用户信息是什么,过程中是否有报错。所以还需要实现ASAuthorizationControllerDelegate protocol的代理方法。
    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let controller = ASAuthorizationController(authorizationRequests: [request])
    authorizationController.delegate = self
    authorizationController.presentationContextProvider = self
    authorizationController.performRequests()

    extension LoginViewController: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    }
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
    }
    }

    extension LoginViewController: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
    return self.view.window!
    }
    }
  3. ASAuthorizationAppleIDCredential
    重头戏来了。用户授权之后,我们要得到请求的用户信息,即fullName和email,还有可以定义用户唯一性的userID,就是通过ASAuthorizationAppleIDCredential对象获得。上面已经说到,用户授权成功后会回调授权成功的代理方法,其中返回的ASAuthorization对象为此次授权行为的抽象类,此对象的credential属性,即为ASAuthorizationAppleIDCredential对象,标志授权之后的凭据。ASAuthorizationAppleIDCredential对象中包含几个重要信息。

    • fullName,全名,包括名和姓。因为用户在授权的时候,可以使用AppleID的全名,可以修改自己授权的全名,所以得到全名并一定是真实的。

    • email,邮箱。邮箱地址也是一个处理过的地址,并不是真正的邮箱地址。这里也是为了避免隐私泄露。如果想给用户邮箱发邮件,可以发送给本地址,但是需要在开发者后台进行配置,指定的邮箱才能发送成功。如果需要请查询文档。

    • user,用户id。这个id是一个唯一的标志。而且特别的,在同一个开发者下所有的App中,对于同一个用户,这个id是唯一的。跨开发者的App中,对于同一个用户,这个id就不一样了。对于同一个App的同一个用户,如果用户取消了对该App的授权,之后又进行了授权操作,此user id也是不变的。

    • state,验证字段。在之前的授权请求Request中,有一个可选属性为state,这个字段在一一对应的Request和Credential中是相同的,用来对应Request。

    • realUserStatus,用户的真实状态。苹果文档说,通过机器学习等技术,可以分辨用户是真人还是机器人等。该字段值为枚举类型,likeReal(像真人),unknown(无法分辨),unsupported(不认为是真人)。苹果说此字段可以为你对用户提供服务的时候进行参考。

根据上面的代码,我们就可以最简单的方式完成Sign In with Apple了。完整的代码建议看苹果的官方代码。将这个官方代码的bundle id和code signing,provisioning profile更换成自己App的,就可以进行试用演示了。

一个细节

当App第一次申请授权的时候,弹出的授权页面中可以编辑fullName,可以选择是否暴露真实的邮箱地址。所以,当第一次授权成功的时候,我们除了用户id之外,可以得到fullName和email。但之后如果再次进行授权申请,就只能获取到用户id,不能得到其他信息了。这就需要我们在第一次授权完成之后,要同步用户信息至服务端,为用户创建新的App的账户。这样也保证了之后再次登录,系统会找回之前的账户,并应该创建新的账户。

几个例外

苹果的review guideline上也写到,有几种情况,是可以不添加Sign In with Apple的。为了不产生歧义,我将原文粘贴在这里,也可以自己跳过去看

  • Your app exclusively uses your company’s own account setup and sign-in systems.
  • Your app is an education, enterprise, or business app that requires the user to sign in with an existing education or enterprise account.
  • Your app uses a government or industry-backed citizen identification system or electronic ID to authenticate users.
  • Your app is a client for a specific third-party service and users are required to sign in to their mail, social media, or other third-party account directly to access their content.

有可能有问题的是第四条。第四条的意思,我认为是,客户端就是某第三方内容的客户端,通过第三方授权登录来获取第三方内容的。比如之前大家喜欢通过新浪微博的开放API来做自己的微博客户端,就是要通过用户授权登录,然后获取微博时间线,好友关系等等信息的。这样的东西当然没必要支持Sign In with Apple了。如果对此有其他解读的,可以联系我。

深入看看

类似微信或者Sign In with Apple这样的第三方登录,一般都是使用OAuth2.0这样的实现方式。什么是OAuth2.0,我也不是很精通,想学习大家可以Google一下。究竟Sign In with Apple这个过程究竟是什么样的?我们只是调用了系统API,在背后有哪些我们不知道的呢?我们稍微来探索一下。

为了说清楚,我从一个Web App支持Sign In with Apple的角度来说。这样就不是单纯的调用系统API了。如果想给Web支持Sign In with Apple,需要在开发者后台再建立一个Service ID。如果感兴趣自己搞搞可以看这里

告诉苹果你是谁

想实现第三方登录,当然我们需要现在三方的开放平台上注册应用,告诉三方哪个应用需要三方登录,告诉三方你是谁。所以像微信需要注册你的App,微信给你分发AppKey和AppSecret,之后请求授权等需要带着这些参数,他们就知道你是谁了;苹果这边你需要在开发者后台操作一番,但没有明显地给你分发AppKey和AppSecret,为啥呢?因为苹果管AppKey叫client_id,对于App就是App Bundle ID,对于Web就是上面创建Web Service ID时候的AppID。AppSecret后面来说。

启动授权获取授权码

请求授权,是授权过程中比较重要的步骤。如果是Web,第三方平台都有授权Web页面,比如跳转微信登录Web页面,苹果也会有自己授权Web页面https://appleid.apple.com/auth/authorize。当然,在跳转这个授权页面的时候,通过GET方式传递几个必要参数,主要有:

  • client_id,标识你是那个App。

  • scope,请求授权的范围。Sign In with Apple的scope只支持 name 和 email。

  • redirect_uri,回调url。这个大家好像不陌生,其他三方登录里面好像也有这个。没错,这个就是用户授权成功之后,三方平台会回调这个地址,将授权码返回给你。这个url需要提前配置,Web的Sign In with Apple就是在上面创建Service ID时候配置的。

  • response_type,reponse类型。这个字段的值一般是‘code’,表示要获取授权码。

好了。授权也搞定了。只要用户打开授权页,授权之后,苹果就会回调我们的redirect_uri,授权码作为参数,从而就获取授权码了。

授权码换取授权信息和accessToken

授权码拿到了,怎么换用户已授权的信息呢?不出所料,苹果给了REST API,所以发个请求就拿到了。但请求参数比较多,这里只说一下client_secret。上面说Sign In with Apple的AppSecret,就是这个client_secret。和微信不同,Sign In with Apple这个参数并不是在注册App的时候和分发给你的固定值,是需要你动态生成的,在这里是一个JWT(Json Web Token)。同样,JWT大家感兴趣可以自行Google。JWT是一个加密加签名后的结果,但没加密没签名之前,格式是这样的:

1
2
3
4
5
6
7
8
9
10
11
{
"alg": "ES256",
"kid": "ABC123DEFG"
}
{
"iss": "DEF123GHIJ",
"iat": 1437179036,
"exp": 1493298100,
"aud": "https://appleid.apple.com",
"sub": "com.mytest.app"
}

其中,上面的部分是Header,下面的部分是Payload。这里面的参数不一一解释了,REST API文档里面有。说一下Header里面的kid,是说一个key的id。想想本文介绍的准备工作中,在开发者后台生成一个私钥的部分,原来在这用到了。kid即为这个私钥的ID。JWT需要签名,也是用这个私钥,通过ES256的算法签名的。Header中的alg参数值就是算法名称。这里就说通了。

请求后得到的结果,有accessToken,但这个字段目前没有用,因为没有开放任何其他通过accessToken获取用户信息接口;还有id_token,这个就是实实在在的授权用户信息了,也是JWT的。我们只要decode就可以拿到了😊。

不知道看了上面这些之后,你有没有对授权这个过程有深一些的理解。本文深度有限,只是大致过了一下,有兴趣的同学还请自己动手,丰衣足食

推荐一篇文章,What the Heck is Sign In with Apple?。这篇文章在理解Sign In with Apple方面给了我很多帮助。

结束

以上就是Sign In with Apple的简单的介绍和实现方法。相信Sign In with Apple会有受众,但究竟Sign In with Apple可以起到多大的作用,有多大的影响力,还需要时间来验证。

感谢大家的阅读,欢迎提出问题来交流。也欢迎给我提出宝贵意见,非常感谢。

参考资料

  1. Sign In with Apple get started: 传送门

  2. About Sign In with Apple: 传送门

  3. AuthenticationService: 传送门

  4. Authenticating Users with Sign in with Apple: 传送门

  5. What the Heck is Sign In with Apple?: 传送门