教程:使用本机身份验证在 iOS/macOS 应用中添加登录和注销

适用于绿色圆圈,带有白色复选标记符号,指示以下内容适用于外部租户。 外部租户(了解详细信息

本教程演示如何使用本机身份验证在 iOS/macOS 应用中使用电子邮件一次性密码或用户名和密码登录和注销用户。

在本教程中,你将:

  • 使用电子邮件一次性密码或用户名(电子邮件)和密码登录用户。
  • 注销用户。
  • 处理登录错误

先决条件

登录用户

若要使用电子邮件一次性密码流程登录用户,请捕获电子邮件地址,并发送包含一次性密码的邮件,供用户验证其电子邮件地址。 当用户输入有效的一次性密码时,应用会登录用户。

若要使用“带密码的电子邮件”流登录用户,请捕获电子邮件和密码。 如果用户名和密码有效,应用将登录用户。

若要登录用户,需要:

  1. 创建用户界面 (UI) 以:

    • 从用户处收集电子邮件。 向输入添加验证,以确保用户输入有效的电子邮件地址。
    • 如果使用用户名(电子邮件)和密码登录,请收集密码。
    • 如果使用电子邮件一次性密码登录,请从用户收集电子邮件一次性密码。
    • 如果使用电子邮件一次性密码登录,请添加让用户重新发送一次性密码的按钮。
  2. 在 UI 中添加一个按钮,其选择事件会启动登录,如以下代码片段所示:

        @IBAction func signInPressed(_: Any) {
        guard let email = emailTextField.text else {
            resultTextView.text = "email not set"
            return
        }
    
        let parameters = MSALNativeAuthSignInParameters(username: email)
        nativeAuth.signIn(parameters: parameters, delegate: self)
    }
    

    为了使用“电子邮件一次性密码”流登录用户,我们使用以下代码片段

    nativeAuth.signIn(parameters: parameters, delegate: self)
    

    signIn(parameters:delegate) 方法(通过对传递的委托对象调用其中一个方法来进行异步响应)必须实现 SignInStartDelegate 协议。 我们传递一个 MSALNativeAuthSignInParameters 的实例,其中包含用户在电子邮件提交表单中提供的电子邮箱地址,并传递 self 作为委托对象。

    为了使用“带密码的电子邮件”流登录用户,我们使用以下代码片段

    let parameters = MSALNativeAuthSignInParameters(username: email)
    parameters.password = password
    nativeAuth.signIn(parameters: parameters, delegate: self)
    

    signIn(parameters:delegate) 方法中,传递 MSALNativeAuthSignInParameters 实例,其中包含用户提供的电子邮件地址及其密码,以及符合 SignInStartDelegate 协议的委托对象。 本示例传递 self

  3. 若要在使用“电子邮件一次性密码”流时实现 SignInStartDelegate 协议,请使用以下代码片段

    extension ViewController: SignInStartDelegate {
        func onSignInStartError(error: MSAL.SignInStartError) {
            resultTextView.text = "Error signing in: \(error.errorDescription ?? "no description")"
        }
    
        func onSignInCodeRequired(
            newState: MSAL.SignInCodeRequiredState,
            sentTo: String,
            channelTargetType: MSAL.MSALNativeAuthChannelType,
            codeLength: Int
        ) {
            resultTextView.text = "Verification code sent to \(sentTo)"
        }
    }
    

    signIn(parameters:delegate) 的结果是调用委托方法。 在最常见的场景中,onSignInCodeRequired(newState:sentTo:channelTargetType:codeLength) 被调用以指示已发送代码以验证用户的电子邮件地址。 除了发送代码的位置及其所含位数的一些详细信息之外,此委托方法还具有 newState 类型的 SignInCodeRequiredState 参数,它让我们能够访问以下两个新方法:

    • submitCode(code:delegate)
    • resendCode(delegate)

    使用 submitCode(code:delegate) 提交用户在一次性密码表单中提供的一次性密码,使用以下代码片段:

    newState.submitCode(code: userSuppliedCode, delegate: self)
    

    submitCode(code:delegate) 接受一次性密码和委托参数。 提交代码后,必须通过实现 SignInVerifyCodeDelegate 协议来验证一次性密码。

    若要将 SignInVerifyCodeDelegate 协议实现为类的扩展,请使用以下代码片段:

    extension ViewController: SignInVerifyCodeDelegate {
        func onSignInVerifyCodeError(error: MSAL.VerifyCodeError, newState: MSAL.SignInCodeRequiredState?) {
            resultTextView.text = "Error verifying code: \(error.errorDescription ?? "no description")"
        }
    
        func onSignInCompleted(result: MSALNativeAuthUserAccountResult) {
            resultTextView.text = "Signed in successfully."
            let parameters = MSALNativeAuthGetAccessTokenParameters()
            result.getAccessToken(parameters: parameters, delegate: self)
        }
    }
    

    在最常见的场景中,我们收到对 onSignInCompleted(result) 的调用,它指示用户已登录。 结果可用于检索 access token

    getAccessToken(parameters:delegate) 接受 MSALNativeAuthGetAccessTokenParameters 实例和委托参数,我们必须在 CredentialsDelegate 协议中实现所需的方法。

    在最常见的场景中,我们收到对 onAccessTokenRetrieveCompleted(result) 的调用,它指示用户已获取 access token

    extension ViewController: CredentialsDelegate {
        func onAccessTokenRetrieveError(error: MSAL.RetrieveAccessTokenError) {
            resultTextView.text = "Error retrieving access token"
        }
    
        func onAccessTokenRetrieveCompleted(result: MSALNativeAuthTokenResult) {
            resultTextView.text = "Signed in. Access Token: \(result.accessToken)"
        }
    }
    
    
  4. 若要在使用“带密码的电子邮件”流时实现 SignInStartDelegate 协议,请使用以下代码片段

    extension ViewController: SignInStartDelegate {
        func onSignInStartError(error: MSAL.SignInStartError) {
            resultTextView.text = "Error signing in: \(error.errorDescription ?? "no description")"
        }
    
        func onSignInCompleted(result: MSAL.MSALNativeAuthUserAccountResult) {
            // User successfully signed in
        }
    }
    

    在最常见的场景中,我们收到对 onSignInCompleted(result) 的调用,它指示用户已登录。 结果可用于检索 access token

    getAccessToken(parameters:delegate) 接受 MSALNativeAuthGetAccessTokenParameters 实例和委托参数,我们必须在 CredentialsDelegate 协议中实现所需的方法。

    在最常见的场景中,我们收到对 onAccessTokenRetrieveCompleted(result) 的调用,它指示用户已获取 access token

    extension ViewController: CredentialsDelegate {
        func onAccessTokenRetrieveError(error: MSAL.RetrieveAccessTokenError) {
            resultTextView.text = "Error retrieving access token"
        }
    
        func onAccessTokenRetrieveCompleted(result: MSALNativeAuthTokenResult) {
            resultTextView.text = "Signed in. Access Token: \(result.accessToken)"
        }
    }
    
    

处理登录错误

在登录期间,并非每个操作都会成功。 例如,用户可能尝试使用不存在的电子邮件地址登录,或提交无效的代码。

  1. 若要处理 signIn(parameters:delegate) 方法中的错误,请使用以下代码片段:

    func onSignInStartError(error: MSAL.SignInStartError) {
        if error.isUserNotFound || error.isInvalidUsername {
            resultTextView.text = "Invalid username"
        } else {
            resultTextView.text = "Error signing in: \(error.errorDescription ?? "no description")"
        }
    }
    
  2. 若要处理 submitCode() 方法中的错误,请使用以下代码片段:

    func onSignInVerifyCodeError(error: MSAL.VerifyCodeError, newState: MSAL.SignInCodeRequiredState?) {
        if error.isInvalidCode {
            // Inform the user that the submitted code was incorrect and ask for a new code to be supplied
            let userSuppliedCode = retrieveNewCode()
            newState?.submitCode(code: userSuppliedCode, delegate: self)
        } else {
            resultTextView.text = "Error verifying code: \(error.errorDescription ?? "no description")"
        }
    }
    

    如果用户输入的邮件验证码不正确,那么错误处理程序会加入对可用于提交新验证码的 SignInCodeRequiredState 的引用。 在之前的 SignInVerifyCodeDelegate 协议实现中,我们在处理 onSignInVerifyCodeError(error:newState) 委托函数时简单地显示了错误。

读取 ID 令牌声明

一旦应用获取了 ID 令牌,即可检索与当前帐户关联的声明。 为此,请使用以下代码片段:

func onSignInCompleted(result: MSAL.MSALNativeAuthUserAccountResult) {
   let claims = result.account.accountClaims
   let preferredUsername = claims?["preferred_username"] as? String
}

用于访问声明值的密钥是你在将用户属性添加为令牌声明时指定的名称。

将用户属性添加到令牌声明一文中了解如何将内置属性和自定义属性添加为令牌声明。

注销用户

若要注销用户,请使用对在 MSALNativeAuthUserAccountResult 回调中收到的 onSignInCompleted 的引用,或者使用 getNativeAuthUserAccount() 从缓存中获取任何已登录的帐户,并将引用存储在 accountResult 成员变量中。

  1. 此处所述为项目配置密钥链组。

  2. 将新的成员变量添加到 ViewController 类:var accountResult: MSALNativeAuthUserAccountResult?

  3. 在成功初始化 viewDidLoad 后添加此行来更新 nativeAuth 以检索任何缓存的帐户:accountResult = nativeAuth.getNativeAuthUserAccount()

  4. 更新 signInCompleted 处理程序以存储帐户结果:

    func onSignInCompleted(result: MSALNativeAuthUserAccountResult) {
        resultTextView.text = "Signed in successfully"
    
        accountResult = result
    }
    
  5. 添加“注销”按钮并使用以下代码注销用户:

    @IBAction func signOutPressed(_: Any) {
        guard let accountResult = accountResult else {
            print("Not currently signed in")
            return
        }
    
        accountResult.signOut()
    
        self.accountResult = nil
    
        resultTextView.text = "Signed out"
    }
    

你已成功完成在应用中注销用户所需的所有步骤。 生成并运行应用程序。 如果一切正常,则应能选择“注销”按钮以成功注销。

使用别名或用户名启用登录

你可以允许使用电子邮件地址和密码登录的用户也使用用户名和密码登录。 用户名也称为备用登录标识符,可以是客户 ID、帐户号或你选择用作用户名的另一个标识符。

可以通过 Microsoft Entra 管理中心手动将用户名分配给用户帐户,或者通过 Microsoft 图形 API 在应用中自动分配用户名。

使用 别名或用户名文章中的 步骤让用户使用应用程序中的用户名登录:

  1. 在登录中启用用户名
  2. 在管理中心创建用户并为其设置用户名 ,或通过 添加用户名更新现有用户。 或者,还可以 使用 Microsoft 图形 API 在应用中自动创建和更新用户

配置自定义声明提供程序

如果要将来自外部系统的声明添加到颁发给应用的令牌中,请使用自定义声明提供程序。 自定义声明提供程序由自定义身份验证扩展组成,它会调用外部 REST API 来从外部系统提取声明。

按照配置自定义声明提供程序中的步骤将外部系统中的声明添加到安全令牌中。