Sign In with Apple

image
  • 原文博客地址: Sign In With Apple
  • 在之前的文章iOS13適配深色模式(Dark Mode)中只是簡單提到了關(guān)于Sign In With Apple的問題, 下面就著重介紹一下什么是Apple登錄
  • 對于很多應(yīng)用都會(huì)有自己的賬號(hào)登錄體系, 但是一般都相對繁瑣, 或者用戶會(huì)忘記密碼等, 為此一般都會(huì)接入微信、QQ登錄, 國外應(yīng)用也會(huì)有Google匾乓、Facebook等第三方登錄方式
  • WWDC 2019上, 蘋果要求使用第三方登錄的應(yīng)用也必須接入蘋果賬號(hào)登錄(2020年必須適配)
  • 當(dāng)然了如果你的App沒有提供第三方登錄反肋,那就不用集成; 如果用到了第三方登錄者春,那么需要提供Sign in with Apple

Sign in with Apple

Sign in with Apple makes it easy for users to sign in to your apps and websites using their Apple ID. Instead of filling out forms, verifying email addresses, and choosing new passwords, they can use Sign in with Apple to set up an account and start using your app right away. All accounts are protected with two-factor authentication for superior security, and Apple will not track users’ activity in your app or website.

Make signing in easy

  • Sign In with Apple為用戶提供一種快速安全的登錄方式, 用戶可以輕松登錄開發(fā)者的應(yīng)用和網(wǎng)站
  • 使用Apple登錄可以讓用戶在系統(tǒng)中設(shè)置用戶帳戶,開發(fā)者可以獲取到用戶名稱(Name), 用戶唯一標(biāo)識(shí)符(ID)以及經(jīng)過驗(yàn)證的電子郵件地址(email)
  • Sign In with Apple相關(guān)特性
    • 尊重用戶隱私: 開發(fā)人員僅僅只能獲取到用戶的姓名和郵箱, 蘋果也不會(huì)收集用戶和應(yīng)用交互的任何信息
    • 系統(tǒng)內(nèi)置的安全性:2F雙重驗(yàn)證(Face IDTouch ID)挖息,從此登錄不再需要密碼
    • 簡化賬號(hào)的創(chuàng)建和登錄流程掉丽,無縫跨設(shè)備使用
    • 開發(fā)者可以獲取到已驗(yàn)證過的郵箱作為登錄賬號(hào)或者與用戶進(jìn)行通信(注:用戶可以選擇隱藏真實(shí)郵箱,并使用蘋果提供的虛擬郵箱進(jìn)行授權(quán))
    • 可跨平臺(tái)使用, Apple登錄支持iOS地熄,macOStvOSwatchOS以及JavaScript
    • 更多信息可慘考
      使用Apple登錄

在代碼集成之前還需要做一些準(zhǔn)備工作

  1. 在開發(fā)者網(wǎng)站芯杀,在需要添加Sign in with Apple功能
image
  1. Xcode里面開啟Sign in with Apple功能
Xcode

登錄按鈕

Apple蘋果登錄按鈕, 需要使用ASAuthorizationAppleIDButton類創(chuàng)建添加, 該類是iOS 13蘋果提供的創(chuàng)建Apple登錄按鈕的專屬類

@available(iOS 13.0, *)
open class ASAuthorizationAppleIDButton : UIControl {
    // 初始化方法
    public convenience init(type: ASAuthorizationAppleIDButton.ButtonType, style: ASAuthorizationAppleIDButton.Style)

    // 初始化方法
    public init(authorizationButtonType type: ASAuthorizationAppleIDButton.ButtonType, authorizationButtonStyle style: ASAuthorizationAppleIDButton.Style)

    // 設(shè)置按鈕的圓切角
    open var cornerRadius: CGFloat
}

開始創(chuàng)建Apple登錄按鈕

// apple登錄按鈕
let appleButton = ASAuthorizationAppleIDButton(type: .continue, style: .black)
appleButton.frame = CGRect(x: 100, y: showLabel.frame.maxY + 40, width: 200, height: 50)
appleButton.cornerRadius = 10
appleButton.addTarget(self, action: #selector(appleAction), for: .touchUpInside)
view.addSubview(appleButton)
  • ASAuthorizationAppleIDButton的初始化方法中有兩個(gè)參數(shù)typestyle
  • type是設(shè)置按鈕的類型ASAuthorizationAppleIDButton.ButtonType
  • style設(shè)置按鈕的樣式ASAuthorizationAppleIDButton.Style
  • 可參考官網(wǎng)介紹Sign In with Apple

ButtonType

public enum ButtonType : Int {
    // signIn登錄類型
    case signIn
    // continue類型
    case `continue`

    public static var `default`: ASAuthorizationAppleIDButton.ButtonType { get }
}

不同ButtonType展示效果如下

ButtonType

Style

@available(iOS 13.0, *)
public enum Style : Int {
    
    case white

    case whiteOutline

    case black
}

不同Style展示效果和使用場景如下

Style

cornerRadius

設(shè)置按鈕的圓角大小

// 默認(rèn)值大概5左右, 具體值不知
appleButton.cornerRadius = 10

發(fā)起授權(quán)請求

  • 在創(chuàng)建好登錄按鈕后, 點(diǎn)擊按鈕的操作就是, 根據(jù)用戶登錄的AppleID發(fā)起授權(quán)請求, 并獲得授權(quán)碼
  • iOS 13系統(tǒng)給我們提供了一個(gè)ASAuthorizationAppleIDProvider
  • 該類就是一種基于用戶的AppleID生成用戶的授權(quán)請求的一種機(jī)制
  • 在發(fā)起授權(quán)請求之前, 需要配置要獲取的數(shù)據(jù)權(quán)限范圍(例如:用戶名端考、郵箱等)
  • 為獲取授權(quán)結(jié)果, 還需要設(shè)置回調(diào)代理, 并發(fā)起授權(quán)請求
@objc fileprivate func appleAction() {
    // 基于用戶的Apple ID授權(quán)用戶,生成用戶授權(quán)請求的一種機(jī)制
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    // 創(chuàng)建新的AppleID授權(quán)請求
    let request = appleIDProvider.createRequest()
    // 所需要請求的聯(lián)系信息
    request.requestedScopes = [.fullName, .email]
    // 管理授權(quán)請求的控制器
    let controller = ASAuthorizationController(authorizationRequests: [request])
    // 授權(quán)成功或者失敗的代理
    controller.delegate = self
    // 顯示上下文的代理, 系統(tǒng)可以在上下文中向用戶展示授權(quán)頁面
    controller.presentationContextProvider = self
    // 在控制器初始化期間啟動(dòng)授權(quán)流
    controller.performRequests()
}

delegate

設(shè)置授權(quán)控制器通知授權(quán)請求的成功與失敗的代理

// 代理
weak open var delegate: ASAuthorizationControllerDelegate?

// 代理方法如下
@available(iOS 13.0, *)
public protocol ASAuthorizationControllerDelegate : NSObjectProtocol {
    // 授權(quán)成功的回調(diào)
    optional func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization)

    // 授權(quán)失敗的回調(diào)
    optional func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error)
}

presentationContextProvider

需要向用戶展示授權(quán)頁面時(shí), 需要遵循該協(xié)議

// 顯示上下文的代理, 系統(tǒng)可以在上下文中向用戶展示授權(quán)頁面
weak open var presentationContextProvider: ASAuthorizationControllerPresentationContextProviding?

// 協(xié)議方法
@available(iOS 13.0, *)
public protocol ASAuthorizationControllerPresentationContextProviding : NSObjectProtocol {

    // 該方法返回一個(gè)視圖錨點(diǎn), 告訴代理應(yīng)該在哪個(gè)window 展示內(nèi)容給用戶
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor
}


// 方法執(zhí)行示例
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
    return self.view.window!
}

ASAuthorization

  • 在控制器獲得授權(quán)的成功回調(diào)中, 協(xié)議方法提供了一個(gè)ASAuthorization
  • ASAuthorization是對控制器成功授權(quán)的封裝, 包括兩個(gè)屬性
@available(iOS 13.0, *)
open class ASAuthorization : NSObject {

    
    // 創(chuàng)建發(fā)起成功授權(quán)的發(fā)起者
    open var provider: ASAuthorizationProvider { get }

    // 成功授權(quán)后返回的相關(guān)憑證, 包含授權(quán)后的相關(guān)信息,是一個(gè)協(xié)議
    open var credential: ASAuthorizationCredential { get }
}

ASAuthorizationCredential

是一個(gè)協(xié)議, 在處理授權(quán)成功的結(jié)果中, 需要使用遵循該協(xié)議的類, 有以下三個(gè)

  • ASPasswordCredential: 密碼憑證
  • ASAuthorizationAppleIDCredential: Apple ID身份驗(yàn)證成功產(chǎn)生的憑證
  • ASAuthorizationSingleSignOnCredential: 單點(diǎn)登錄(SSO)身份驗(yàn)證產(chǎn)生的憑據(jù)

ASPasswordCredential

@available(iOS 12.0, *)
open class ASPasswordCredential : NSObject, ASAuthorizationCredential {

    // 初始化方法
    public init(user: String, password: String)

    // 用戶名
    open var user: String { get }

    // 用戶密碼
    open var password: String { get }
}

ASAuthorizationAppleIDCredential

@available(iOS 13.0, *)
open class ASAuthorizationAppleIDCredential : NSObject, ASAuthorizationCredential {

    // 和用戶AppleID關(guān)聯(lián)的用戶ID(標(biāo)識(shí)符)
    open var user: String { get }

    // 傳送給ASAuthorizationRequest的字符串
    open var state: String? { get }

    // 用戶授權(quán)的可訪問的聯(lián)系信息的種類
    open var authorizedScopes: [ASAuthorization.Scope] { get }

    // 為APP提供的授權(quán)證明的有效token
    open var authorizationCode: Data? { get }

    // JSON Web Token (JWT), 用于以安全的方式向應(yīng)用程序傳遞關(guān)于用戶身份的信息
    open var identityToken: Data? { get }

    // 用戶的email
    open var email: String? { get }

    // 用戶名
    open var fullName: PersonNameComponents? { get }

    // 用戶是否是真實(shí)用戶的狀態(tài)
    open var realUserStatus: ASUserDetectionStatus { get }
}

// 用戶是否是真實(shí)用戶的枚舉值
public enum ASUserDetectionStatus : Int {

    case unsupported

    case unknown

    case likelyReal
}

ASAuthorizationSingleSignOnCredential

@available(iOS 13.0, *)
open class ASAuthorizationSingleSignOnCredential : NSObject, ASAuthorizationCredential {

    // AuthenticationServices返回的字符串
    open var state: String? { get }

    // 用戶獲取授權(quán)范圍的token
    open var accessToken: Data? { get }

    // JSON Web Token (JWT), 用于以安全的方式向應(yīng)用程序傳遞關(guān)于用戶身份的信息
    open var identityToken: Data? { get }

    // 用戶授權(quán)的可訪問的聯(lián)系信息的種類
    open var authorizedScopes: [ASAuthorization.Scope] { get }

    // 完整的身份驗(yàn)證響應(yīng)信息
    @NSCopying open var authenticatedResponse: HTTPURLResponse? { get }
}

授權(quán)成功

上面有提到, 在ASAuthorizationControllerDelegate有兩個(gè)協(xié)議方法, 分別是授權(quán)成功和失敗的回調(diào), 下面就具體看看

extension SignViewController: ASAuthorizationControllerDelegate {
    // 處理成功的授權(quán)
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        print("授權(quán)成功")
        // 成功的Apple ID身份驗(yàn)證信息
        if let appleIDCreden = authorization.credential as? ASAuthorizationAppleIDCredential {
            let userIdentifier = appleIDCreden.user
            let fullName = appleIDCreden.fullName
            let email = appleIDCreden.email
            
            // 這里需要我們在系統(tǒng)中創(chuàng)建一個(gè)賬戶, 用于存儲(chǔ)用戶的唯一標(biāo)識(shí)userIdentifier
            // 可以在系統(tǒng)的鑰匙串中存儲(chǔ)
            let webVC = WebViewController()
            webVC.user = userIdentifier
            webVC.giveName = fullName?.givenName ?? ""
            webVC.familyName = fullName?.familyName ?? ""
            webVC.email = email ?? ""
            navigationController?.pushViewController(webVC, animated: true)
        } else if let passwordCreden = authorization.credential as? ASPasswordCredential {
            // 密碼憑證用戶的唯一標(biāo)識(shí)
            let userIdentifiler = passwordCreden.user
            // 密碼憑證的密碼
            let password = passwordCreden.password
            
            // 顯示相關(guān)信息
            let message = "APP已經(jīng)收到您選擇的秘鑰憑證\nUsername: \(userIdentifiler)\n Password: \(password)"
            showLabel.text = message
        } else {
            showLabel.text = "授權(quán)信息均不符"
        }
    }
    
    // 處理授權(quán)錯(cuò)誤
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        print("授權(quán)錯(cuò)誤: \(error)")
        
        var showText = ""
        if let authError = error as? ASAuthorizationError {
            let code = authError.code
            switch code {
            case .canceled:
                showText = "用戶取消了授權(quán)請求"
            case .failed:
                showText = "授權(quán)請求失敗"
            case .invalidResponse:
                showText = "授權(quán)請求響應(yīng)無效"
            case .notHandled:
                showText = "未能處理授權(quán)請求"
            case .unknown:
                showText = "授權(quán)請求失敗, 未知的錯(cuò)誤原因"
            default:
                showText = "其他未知的錯(cuò)誤原因"
            }
        }
        showLabel.text = showText
    }
}

做好了上面配置, 就可以看到下面的登錄頁面

image
  • 如果不修改姓名, 授權(quán)成功后將獲取到用戶的姓名
  • 如果選擇共享我的電子郵件, 授權(quán)成功將獲取到用戶的電子郵件地址
  • 如果選擇隱藏郵件地址, 授權(quán)成功將獲取到一個(gè)虛擬的電子郵件地址
  • 點(diǎn)擊姓名右側(cè)的清除按鈕可以修改用戶名, 如下頁面
name
  • 如果登錄用戶修改了用戶名, 那么授權(quán)成功后獲取到的用戶名就是修改后的
  • 使用過AppleID登錄過App揭厚,進(jìn)入應(yīng)用的時(shí)候會(huì)提示使用TouchID登錄的場景如下
image
  • 如果使用指紋登錄三次失敗后, 下面會(huì)有一個(gè)使用密碼繼續(xù)的按鈕, 可以使用手機(jī)密碼繼續(xù)登錄
  • 如果手機(jī)沒有設(shè)置Apple ID, 使用蘋果登錄, 將會(huì)有彈窗提示,
image

監(jiān)聽授權(quán)狀態(tài)

  • 在特殊情況下我們還需要監(jiān)聽授權(quán)狀態(tài)的改變, 并進(jìn)行相應(yīng)的處理
  • 用戶終止在該App中使用Sign in with Apple功能
  • 用戶在設(shè)置里注銷了Apple ID
  • 針對類似這種情況, App需要獲取到這些狀態(tài)却特,然后做退出登錄操作
  • 我們需要在App啟動(dòng)的時(shí)候,來獲取當(dāng)前用戶的授權(quán)狀態(tài)
// ASAuthorizationAppleIDProvider提供了一個(gè)獲取用戶授權(quán)狀態(tài)和授權(quán)憑據(jù)是否有效
func getCredentialState(forUserID: String, completion: (ASAuthorizationAppleIDProvider.CredentialState, Error?) -> Void)


// ASAuthorizationAppleIDProvider.CredentialState的所有枚舉值
public enum CredentialState : Int {
    case revoked
    case authorized
    case notFound
    case transferred
}

示例代碼如下

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    if #available(iOS 13.0, *) {
        // 鑰匙串中取出的
        let userIdentifier = "userIdentifier"
        if (!userIdentifier.isEmpty) {
            // 基于用戶的Apple ID授權(quán)用戶棋弥,生成用戶授權(quán)請求的一種機(jī)制
            let appleIDProvider = ASAuthorizationAppleIDProvider()
            // 返回完成處理程序中給定用戶的憑據(jù)狀態(tài)
            appleIDProvider.getCredentialState(forUserID: userIdentifier) { (state, error) in
                switch state {
                case .authorized:
                    print("授權(quán)狀態(tài)有效")
                case .notFound:
                    print("授權(quán)憑證缺失(可能是使用AppleID 登錄過App)")
                case .revoked:
                    print("上次使用蘋果賬號(hào)登錄的憑據(jù)已被移除核偿,需解除綁定并重新引導(dǎo)用戶使用蘋果登錄")
                default:
                    print("未知狀態(tài)")
                }
            }
        }
    }
    
    return true
}

除此之外還可以通過通知方法來監(jiān)聽revoked狀態(tài), 在ASAuthorizationAppleIDProvider中增加了一個(gè)屬性, 用于監(jiān)聽revoked狀態(tài)

@available(iOS 13.0, *)
public class let credentialRevokedNotification: NSNotification.Name

// 使用方法
fileprivate func observeAppleSignInState() {
    if #available(iOS 13.0, *) {
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(handleStateChange(noti:)), name: ASAuthorizationAppleIDProvider.credentialRevokedNotification, object: nil)
    }
}

@objc fileprivate func handleStateChange(noti: Any) {
    print("授權(quán)狀態(tài)發(fā)生改變")
}

參考文檔


歡迎您掃一掃下面的微信公眾號(hào),訂閱我的博客顽染!

微信公眾號(hào)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末漾岳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子粉寞,更是在濱河造成了極大的恐慌尼荆,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唧垦,死亡現(xiàn)場離奇詭異捅儒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)振亮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門巧还,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坊秸,你說我怎么就攤上這事麸祷。” “怎么了褒搔?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵阶牍,是天一觀的道長。 經(jīng)常有香客問我星瘾,道長走孽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任琳状,我火速辦了婚禮磕瓷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘念逞。我一直安慰自己生宛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布肮柜。 她就那樣靜靜地躺著陷舅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪审洞。 梳的紋絲不亂的頭發(fā)上莱睁,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音芒澜,去河邊找鬼仰剿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛痴晦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播誊酌,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼劳较!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厂画,沒想到半個(gè)月后瞭稼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欲虚,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡腌零,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浅辙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丁寄。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡删咱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出团搞,到底是詐尸還是另有隱情峻黍,我是刑警寧澤挽拂,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布仑扑,位于F島的核電站,受9級(jí)特大地震影響俱济,放射性物質(zhì)發(fā)生泄漏辖源。R本人自食惡果不足惜矾湃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一霉咨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拍屑,春花似錦途戒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至矢渊,卻和暖如春继准,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背矮男。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工移必, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人毡鉴。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓崔泵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親猪瞬。 傳聞我的和親對象是個(gè)殘疾皇子憎瘸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容