教程:使用本机身份验证在 iOS/macOS 应用中调用多个 API

适用于带白色勾号的绿色圆圈。iOS (Swift)带白色对号的绿色圆圈。 macOS (Swift)

本教程介绍如何在 iOS/macOS 应用中获取访问令牌并调用 API。 适用于 iOS/macOS 的 Microsoft 身份验证库 (MSAL) 本机身份验证 SDK 允许通过一次登录获取多个访问令牌。 借助此功能,你可获取一个或多个访问令牌,而无需用户重新进行身份验证。

本教程介绍如何执行下列操作:

  • 获取一个或多个访问令牌。
  • 调用 API

先决条件

获取一个或多个访问令牌

MSAL 本机身份验证 SDK 可以存储多个访问令牌。 登录后,可以使用 getAccessToken(scope:) 函数获取访问令牌,并指定要授予的新访问令牌的范围。

  1. 使用以下代码片段声明和设置一组 API 范围的值:

    let protectedAPIUrl1: String? = nil
    let protectedAPIUrl2: String? = nil 
    let protectedAPIScopes1: [String] = []
    let protectedAPIScopes2: [String] = []
    
    var accessTokenAPI1: String?
    var accessTokenAPI2: String?
    
    • 使用第一个 Web API 的 URL 初始化 protectedAPIUrl1
    • 使用第二个 Web API 的 URL 初始化 protectedAPIUrl2
    • 使用第一个 API 的范围定义 protectedAPIScopes1 ,例如 ["api://<Resource_App_ID>/ToDoList.Read", "api://<Resource_App_ID>/ToDoList.ReadWrite"]
    • 使用第二个 API 的范围定义 protectedAPIScopes2,类似于 protectedAPIScopes1
    • 声明可选的字符串变量 accessTokenAPI1accessTokenAPI2
  2. 使用以下代码片段让用户登录:

    @IBAction func signInPressed(_: Any) {
        guard let email = emailTextField.text, let password = passwordTextField.text else {
            resultTextView.text = "Email or password not set"
            return
        }
    
        print("Signing in with email \(email) and password")
    
        showResultText("Signing in...")
    
        nativeAuth.signIn(username: email, password: password, delegate: self)
    }
    

    signInPressed 方法处理登录按钮的按下。 它会检查电子邮件和密码字段是否已填写。 如果其中一个为空,则显示“未设置电子邮件或密码”。如果这两个字段均已填写,则会显示“正在登录...”,并使用 nativeAuth 中的 signIn 方法以及提供的电子邮件和密码启动登录。 因为未指定任何范围,所以SDK 会检索对默认 OIDC 范围 (openid、offline_access、profile) 有效的令牌。

  3. 使用以下代码片段获取一个或多个访问令牌:

    @IBAction func protectedApi1Pressed(_: Any) {
        guard let url = protectedAPIUrl1, !protectedAPIScopes1.isEmpty else {
            showResultText("API 1 not configured.")
            return
        }
    
        if let accessToken = accessTokenAPI1 {
            accessProtectedAPI(apiUrl: url, accessToken: accessToken)
        } else {
            accountResult?.getAccessToken(scopes: protectedAPIScopes1, delegate: self)
            let message = "Retrieving access token to use with API 1..."
            showResultText(message)
            print(message)
        }
    }
    
    @IBAction func protectedApi2Pressed(_: Any) {
        guard let url = protectedAPIUrl2, !protectedAPIScopes2.isEmpty else {
            showResultText("API 2 not configured.")
            return
        }
    
        if let accessToken = accessTokenAPI2 {
            accessProtectedAPI(apiUrl: url, accessToken: accessToken)
        } else {
            accountResult?.getAccessToken(scopes: protectedAPIScopes2, delegate: self)
            let message = "Retrieving access token to use with API 2..."
            showResultText(message)
            print(message)
        }
    }
    

    这些 protectedApi1PressedprotectedApi2Pressed 方法管理为两组不同的范围获取访问令牌的过程。 它们首先确保正确配置了每个 API 的 URL 和范围。 如果 API 的访问令牌已可用,它将直接访问该 API。 否则,它会请求访问令牌,并通知用户正在进行的令牌检索过程。

    若要将访问令牌分配给 protectedAPIScopes1protectedAPIScopes2,请使用以下代码片段:

    func onAccessTokenRetrieveCompleted(result: MSALNativeAuthTokenResult) {
        print("Access Token: \(result.accessToken)")
    
        if protectedAPIScopes1.allSatisfy(result.scopes.contains),
           let url = protectedAPIUrl1
        {
            accessTokenAPI1 = result.accessToken
            accessProtectedAPI(apiUrl: url, accessToken: result.accessToken)
        }
    
        if protectedAPIScopes2.allSatisfy(result.scopes.contains(_:)),
           let url = protectedAPIUrl2
        {
            accessTokenAPI2 = result.accessToken
            accessProtectedAPI(apiUrl: url, accessToken: result.accessToken)
        }
    
        showResultText("Signed in." + "\n\n" + "Scopes:\n\(result.scopes)" + "\n\n" + "Access Token:\n\(result.accessToken)")
        updateUI()
    }
    
    func onAccessTokenRetrieveError(error: MSAL.RetrieveAccessTokenError) {
        showResultText("Error retrieving access token: \(error.errorDescription ?? "No error description")")
    }
    

    onAccessTokenRetrieveCompleted方法将访问令牌输出到控制台。 然后,它检查 protectedAPIScopes1 是否包含在结果范围内以及 protectedAPIUrl1 是否可用,如果是,则使用 URL 和令牌设置 accessTokenAPI1 并调用 accessProtectedAPI。 如果满足条件,它将为 protectedAPIScopes2protectedAPIUrl2 执行类似的检查,更新 accessTokenAPI2 并调用 API。 最后,该方法将显示一条消息,其中包含已登录状态、范围和访问令牌,并更新 UI。

    onAccessTokenRetrieveError 方法将显示一条错误消息,其中包含访问令牌检索错误的说明,如果未提供任何说明,则显示默认消息。

调用 API

使用以下代码片段调用 API:

func accessProtectedAPI(apiUrl: String, accessToken: String) {
    guard let url = URL(string: apiUrl) else {
        let errorMessage = "Invalid API url"
        print(errorMessage)
        DispatchQueue.main.async {
            self.showResultText(errorMessage)
        }
        return
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
    
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            print("Error found when accessing API: \(error.localizedDescription)")
            DispatchQueue.main.async {
                self.showResultText(error.localizedDescription)
            }
            return
        }
        
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode)
        else {
            DispatchQueue.main.async {
                self.showResultText("Unsuccessful response found when accessing the API")
            }
            return
        }
        
        guard let data = data, let result = try? JSONSerialization.jsonObject(with: data, options: []) else {
            DispatchQueue.main.async {
                self.showResultText("Couldn't deserialize result JSON")
            }
            return
        }
        
        DispatchQueue.main.async {
            self.showResultText("""
                            Accessed API successfully using access token.
                            HTTP response code: \(httpResponse.statusCode)
                            HTTP response body: \(result)
                            """)
        }
    }
    
    task.resume()
}

accessProtectedAPI 方法使用提供的访问令牌将 GET 请求发送到指定的 API 终结点。 它使用“授权标头”中的令牌配置请求。 当它收到成功响应(HTTP 状态代码 200-299)时,它会反序列化 JSON 数据,并使用 HTTP 状态代码和响应正文更新 UI。 如果在请求或响应处理期间发生错误,它会在 UI 中显示错误消息。 此方法允许访问 API 1 或 API 2,具体取决于提供的 URL 和访问令牌。