教程:从 iOS 或 macOS 应用登录用户并调用 Microsoft Graph
在本教程中,你将生成一个与 Microsoft 标识平台集成的 iOS 或 macOS 应用,用于使用户登录并获取用于调用 Microsoft Graph API 的访问令牌。
完成本教程后,应用程序将接受个人 Microsoft 帐户(包括 outlook.com、live.com 和其他平台)和使用 Microsoft Entra ID 的任何公司或组织的工作或学校帐户进行登录。 本教程适用于 iOS 和 macOS 应用。 这两个平台之间的某些步骤有所不同。
本教程的内容:
- 在 Xcode 中创建 iOS 或 macOS 应用项目
- 在 Microsoft Entra 管理中心注册应用
- 添加代码以支持用户登录和注销
- 添加代码以调用 Microsoft Graph API
- 测试应用程序
先决条件
教程应用的工作方式
本教程中的应用可以将用户登录并代表用户从 Microsoft Graph 获取数据。 此数据通过一个受保护 API(在本例中为 Microsoft Graph API)进行访问,该 API 要求授权并且受 Microsoft 标识平台保护。
更具体说来:
- 你的应用通过浏览器或 Microsoft Authenticator 使用户登录。
- 最终用户接受你的应用程序请求的权限。
- 为你的应用颁发一个 Microsoft Graph API 的访问令牌。
- 该访问令牌包含在对 Web API 的 HTTP 请求中。
- 处理 Microsoft Graph 响应。
此示例使用 Microsoft 身份验证库 (MSAL) 来实现身份验证。 MSAL 将自动续订令牌,在设备上的其他应用之间提供单一登录 (SSO),还将管理帐户。
若要下载在此教程中构建的应用的完整版本,可在 GitHub 中找到这两个版本:
- iOS 代码示例 (GitHub)
- macOS 代码示例 (GitHub)
创建新项目
- 打开 Xcode,并选择“新建 Xcode 项目”。
- 对于 iOS 应用,请选择“iOS”>“单一视图应用”并选择“下一步”。
- 对于 macOS 应用,请选择“macOS”>“Cocoa 应用”并选择“下一步”。
- 提供产品名称。
- 将“语言”设置为“Swift”,然后选择“下一步”。
- 选择用于创建应用的文件夹,并选择“创建”。
注册应用程序
提示
本文中的步骤可能因开始使用的门户而略有不同。
- 至少以应用程序开发人员的身份登录到 Microsoft Entra 管理中心。
- 如果你有权访问多个租户,请使用顶部菜单中的“设置”图标 ,通过“目录 + 订阅”菜单切换到你希望在其中注册应用程序的租户。
- 浏览到“标识”>“应用程序”>“应用注册”。
- 选择“新注册”。
- 输入应用程序的名称。 应用的用户可能会看到此名称,你稍后可对其进行更改。
- 在“受支持的帐户类型”下,选择“任何组织目录(任何 Microsoft Entra 目录 - 多租户)中的帐户和个人 Microsoft 帐户(例如,Skype、Xbox)”。
- 选择“注册”。
- 在“管理”下,选择“身份验证”>“添加平台”>“iOS/macOS” 。
- 输入项目的捆绑 ID。 如果下载了代码示例,则捆绑 ID 为
com.microsoft.identitysample.MSALiOS
。 若要创建自己的项目,请在 Xcode 中选择项目,然后打开“常规”选项卡。此时捆绑标识符会显示在“标识”部分。 - 选择“配置”并保存出现在“MSAL 配置”页中的“MSAL 配置”,以便在稍后配置应用时输入它 。
- 选择“完成”。
添加 MSAL
选择以下方式之一在应用中安装 MSAL 库:
CocoaPods
如果使用 CocoaPods,请安装
MSAL
,方法是先在项目的 .xcodeproj 文件所在的文件夹中创建名为 podfile 的空文件。 将以下命令添加到 podfile:use_frameworks! target '<your-target-here>' do pod 'MSAL' end
将
<your-target-here>
替换为项目的名称。在终端窗口中导航到包含所创建的 podfile 的文件夹,然后运行
pod install
以安装 MSAL 库。关闭 Xcode,然后打开
<your project name>.xcworkspace
,以便在 Xcode 中重新加载项目。
Carthage
如果使用 Carthage,请安装 MSAL
,只需将其添加到 Cartfile 即可:
github "AzureAD/microsoft-authentication-library-for-objc" "master"
在终端窗口中,在与更新的 Cartfile 相同的目录中,运行以下命令,让 Carthage 更新项目中的依赖项。
iOS:
carthage update --platform iOS
macOS:
carthage update --platform macOS
手动
还可使用 Git 子模块或查看最新版本,以便在应用程序中将其用作框架。
添加应用注册
接下来,我们将你的应用注册添加到代码中。
首先,将以下导入语句添加到 ViewController.swift 文件和 AppDelegate.swift 或 SceneDelegate.swift 的顶部:
import MSAL
接下来,在 viewDidLoad()
前面将以下代码添加到 ViewController.swift:
// Update the below to your client ID. The below is for running the demo only
let kClientID = "Your_Application_Id_Here"
let kGraphEndpoint = "https://graph.microsoft.com/" // the Microsoft Graph endpoint
let kAuthority = "https://login.microsoftonline.com/common" // this authority allows a personal Microsoft account and a work or school account in any organization's Azure AD tenant to sign in
let kScopes: [String] = ["user.read"] // request permission to read the profile of the signed-in user
var accessToken = String()
var applicationContext : MSALPublicClientApplication?
var webViewParameters : MSALWebviewParameters?
var currentAccount: MSALAccount?
你修改的唯一值是分配到 kClientID
以用作你的应用程序 ID 的值。 此值是你在本教程开头用于注册应用程序的步骤中保存的 MSAL 配置数据的一部分。
配置 Xcode 项目设置
将新的密钥链组添加到项目的“签名和功能”。 密钥链组在 iOS 上应为 com.microsoft.adalcache
,在 macOS 上应为 com.microsoft.identity.universalstorage
。
仅对于 iOS,配置 URL 方案
在此步骤中需注册 CFBundleURLSchemes
,以便用户在登录后可重定向回应用。 另外,LSApplicationQueriesSchemes
也允许应用使用 Microsoft Authenticator。
在 Xcode 中将 Info.plist 作为源代码文件打开,在 <dict>
部分中添加以下命令。 将 [BUNDLE_ID]
替换为你之前使用的值。 如果已下载代码,则捆绑包标识符为 com.microsoft.identitysample.MSALiOS
。 若要创建自己的项目,请在 Xcode 中选择项目,然后打开“常规”选项卡。此时捆绑标识符会显示在“标识”部分。
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>msauth.[BUNDLE_ID]</string>
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>msauthv2</string>
<string>msauthv3</string>
</array>
仅对于 macOS,配置应用沙盒
- 转到 Xcode 项目设置 >“功能”选项卡>“应用沙盒”
- 选中“传出连接(客户端)”复选框。
创建应用的 UI
现在,请将以下代码添加到 ViewController
类,以便创建一个 UI,其中包含用于调用 Microsoft Graph API 的按钮,用于退出登录的按钮,以及用于查看某些输出的文本视图:
iOS UI
var loggingText: UITextView!
var signOutButton: UIButton!
var callGraphButton: UIButton!
var usernameLabel: UILabel!
func initUI() {
usernameLabel = UILabel()
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
usernameLabel.text = ""
usernameLabel.textColor = .darkGray
usernameLabel.textAlignment = .right
self.view.addSubview(usernameLabel)
usernameLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 50.0).isActive = true
usernameLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10.0).isActive = true
usernameLabel.widthAnchor.constraint(equalToConstant: 300.0).isActive = true
usernameLabel.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
// Add call Graph button
callGraphButton = UIButton()
callGraphButton.translatesAutoresizingMaskIntoConstraints = false
callGraphButton.setTitle("Call Microsoft Graph API", for: .normal)
callGraphButton.setTitleColor(.blue, for: .normal)
callGraphButton.addTarget(self, action: #selector(callGraphAPI(_:)), for: .touchUpInside)
self.view.addSubview(callGraphButton)
callGraphButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
callGraphButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 120.0).isActive = true
callGraphButton.widthAnchor.constraint(equalToConstant: 300.0).isActive = true
callGraphButton.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
// Add sign out button
signOutButton = UIButton()
signOutButton.translatesAutoresizingMaskIntoConstraints = false
signOutButton.setTitle("Sign Out", for: .normal)
signOutButton.setTitleColor(.blue, for: .normal)
signOutButton.setTitleColor(.gray, for: .disabled)
signOutButton.addTarget(self, action: #selector(signOut(_:)), for: .touchUpInside)
self.view.addSubview(signOutButton)
signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
signOutButton.topAnchor.constraint(equalTo: callGraphButton.bottomAnchor, constant: 10.0).isActive = true
signOutButton.widthAnchor.constraint(equalToConstant: 150.0).isActive = true
signOutButton.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
let deviceModeButton = UIButton()
deviceModeButton.translatesAutoresizingMaskIntoConstraints = false
deviceModeButton.setTitle("Get device info", for: .normal);
deviceModeButton.setTitleColor(.blue, for: .normal);
deviceModeButton.addTarget(self, action: #selector(getDeviceMode(_:)), for: .touchUpInside)
self.view.addSubview(deviceModeButton)
deviceModeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
deviceModeButton.topAnchor.constraint(equalTo: signOutButton.bottomAnchor, constant: 10.0).isActive = true
deviceModeButton.widthAnchor.constraint(equalToConstant: 150.0).isActive = true
deviceModeButton.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
// Add logging textfield
loggingText = UITextView()
loggingText.isUserInteractionEnabled = false
loggingText.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(loggingText)
loggingText.topAnchor.constraint(equalTo: deviceModeButton.bottomAnchor, constant: 10.0).isActive = true
loggingText.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 10.0).isActive = true
loggingText.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10.0).isActive = true
loggingText.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 10.0).isActive = true
}
func platformViewDidLoadSetup() {
NotificationCenter.default.addObserver(self,
selector: #selector(appCameToForeGround(notification:)),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
@objc func appCameToForeGround(notification: Notification) {
self.loadCurrentAccount()
}
macOS UI
var callGraphButton: NSButton!
var loggingText: NSTextView!
var signOutButton: NSButton!
var usernameLabel: NSTextField!
func initUI() {
usernameLabel = NSTextField()
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
usernameLabel.stringValue = ""
usernameLabel.isEditable = false
usernameLabel.isBezeled = false
self.view.addSubview(usernameLabel)
usernameLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 30.0).isActive = true
usernameLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10.0).isActive = true
// Add call Graph button
callGraphButton = NSButton()
callGraphButton.translatesAutoresizingMaskIntoConstraints = false
callGraphButton.title = "Call Microsoft Graph API"
callGraphButton.target = self
callGraphButton.action = #selector(callGraphAPI(_:))
callGraphButton.bezelStyle = .rounded
self.view.addSubview(callGraphButton)
callGraphButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
callGraphButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 50.0).isActive = true
callGraphButton.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
// Add sign out button
signOutButton = NSButton()
signOutButton.translatesAutoresizingMaskIntoConstraints = false
signOutButton.title = "Sign Out"
signOutButton.target = self
signOutButton.action = #selector(signOut(_:))
signOutButton.bezelStyle = .texturedRounded
self.view.addSubview(signOutButton)
signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
signOutButton.topAnchor.constraint(equalTo: callGraphButton.bottomAnchor, constant: 10.0).isActive = true
signOutButton.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
signOutButton.isEnabled = false
// Add logging textfield
loggingText = NSTextView()
loggingText.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(loggingText)
loggingText.topAnchor.constraint(equalTo: signOutButton.bottomAnchor, constant: 10.0).isActive = true
loggingText.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 10.0).isActive = true
loggingText.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10.0).isActive = true
loggingText.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -10.0).isActive = true
loggingText.widthAnchor.constraint(equalToConstant: 500.0).isActive = true
loggingText.heightAnchor.constraint(equalToConstant: 300.0).isActive = true
}
func platformViewDidLoadSetup() {}
接下来,还是在 ViewController
类中,将 viewDidLoad()
方法替换为:
override func viewDidLoad() {
super.viewDidLoad()
initUI()
do {
try self.initMSAL()
} catch let error {
self.updateLogging(text: "Unable to create Application Context \(error)")
}
self.loadCurrentAccount()
self.platformViewDidLoadSetup()
}
使用 MSAL
初始化 MSAL
将 initMSAL
方法添加到 ViewController
类:
func initMSAL() throws {
guard let authorityURL = URL(string: kAuthority) else {
self.updateLogging(text: "Unable to create authority URL")
return
}
let authority = try MSALAADAuthority(url: authorityURL)
let msalConfiguration = MSALPublicClientApplicationConfig(clientId: kClientID, redirectUri: nil, authority: authority)
self.applicationContext = try MSALPublicClientApplication(configuration: msalConfiguration)
self.initWebViewParams()
}
仍在 ViewController
类中,在 initMSAL
方法后面,添加 initWebViewParams
方法:
iOS 代码:
func initWebViewParams() {
self.webViewParameters = MSALWebviewParameters(authPresentationViewController: self)
}
macOS 代码:
func initWebViewParams() {
self.webViewParameters = MSALWebviewParameters()
}
处理登录回叫(仅限 iOS)
打开 AppDelegate.swift 文件。 若要在登录后处理回叫,请在 appDelegate
类中添加 MSALPublicClientApplication.handleMSALResponse
,如下所示:
// Inside AppDelegate...
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String)
}
如果使用的是 Xcode 11,则应改为将 MSAL 回叫放入 SceneDelegate.swift 中。 如果支持兼容旧版 iOS 的 UISceneDelegate 和 UIApplicationDelegate,则需将 MSAL 回叫置于这两个文件中。
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let urlContext = URLContexts.first else {
return
}
let url = urlContext.url
let sourceApp = urlContext.options.sourceApplication
MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: sourceApp)
}
获取令牌
现在,我们可以实现应用程序的 UI 处理逻辑并通过 MSAL 以交互方式获取令牌。
MSAL 公开了获取令牌的两种主要方法:acquireTokenSilently()
和 acquireTokenInteractively()
。
只要有帐户,
acquireTokenSilently()
就会尝试以用户身份登录并获取令牌,而无需与用户交互。acquireTokenSilently()
需要有效的MSALAccount
,可使用某个 MSAL 的帐户枚举 API 检索它。 本教程使用applicationContext.getCurrentAccount(with: msalParameters, completionBlock: {})
检索当前帐户。acquireTokenInteractively()
在尝试登录用户时始终显示 UI。 它可能会使用浏览器中的会话 Cookie 或 Microsoft Authenticator 中的帐户来提供交互式 SSO 体验。
将以下代码添加到 ViewController
类:
func getGraphEndpoint() -> String {
return kGraphEndpoint.hasSuffix("/") ? (kGraphEndpoint + "v1.0/me/") : (kGraphEndpoint + "/v1.0/me/");
}
@objc func callGraphAPI(_ sender: AnyObject) {
self.loadCurrentAccount { (account) in
guard let currentAccount = account else {
// We check to see if we have a current logged in account.
// If we don't, then we need to sign someone in.
self.acquireTokenInteractively()
return
}
self.acquireTokenSilently(currentAccount)
}
}
typealias AccountCompletion = (MSALAccount?) -> Void
func loadCurrentAccount(completion: AccountCompletion? = nil) {
guard let applicationContext = self.applicationContext else { return }
let msalParameters = MSALParameters()
msalParameters.completionBlockQueue = DispatchQueue.main
applicationContext.getCurrentAccount(with: msalParameters, completionBlock: { (currentAccount, previousAccount, error) in
if let error = error {
self.updateLogging(text: "Couldn't query current account with error: \(error)")
return
}
if let currentAccount = currentAccount {
self.updateLogging(text: "Found a signed in account \(String(describing: currentAccount.username)). Updating data for that account...")
self.updateCurrentAccount(account: currentAccount)
if let completion = completion {
completion(self.currentAccount)
}
return
}
self.updateLogging(text: "Account signed out. Updating UX")
self.accessToken = ""
self.updateCurrentAccount(account: nil)
if let completion = completion {
completion(nil)
}
})
}
以交互方式获取令牌
以下代码片段通过创建 MSALInteractiveTokenParameters
对象并调用 acquireToken
来首次获取令牌。 接下来添加符合以下条件的代码:
- 使用作用域创建
MSALInteractiveTokenParameters
。 - 使用创建的参数调用
acquireToken()
。 - 处理错误。 有关更多详细信息,请参阅适用于 iOS 和 macOS 的 MSAL 错误处理指南。
- 处理成功案例。
将以下代码添加到 ViewController
类。
func acquireTokenInteractively() {
guard let applicationContext = self.applicationContext else { return }
guard let webViewParameters = self.webViewParameters else { return }
// #1
let parameters = MSALInteractiveTokenParameters(scopes: kScopes, webviewParameters: webViewParameters)
parameters.promptType = .selectAccount
// #2
applicationContext.acquireToken(with: parameters) { (result, error) in
// #3
if let error = error {
self.updateLogging(text: "Could not acquire token: \(error)")
return
}
guard let result = result else {
self.updateLogging(text: "Could not acquire token: No result returned")
return
}
// #4
self.accessToken = result.accessToken
self.updateLogging(text: "Access token is \(self.accessToken)")
self.updateCurrentAccount(account: result.account)
self.getContentWithToken()
}
}
MSALInteractiveTokenParameters
的 promptType
属性会配置身份验证和同意提示行为。 支持以下值:
.promptIfNecessary
(默认值)- 仅在必要时才提示用户。 SSO 体验取决于 Web 视图中是否存在 Cookie,此外还取决于帐户类型。 如果有多个用户已登录,则会显示帐户选择体验。 这是默认行为。.selectAccount
- 如果未指定用户,则身份验证 Web 视图会显示当前已登录帐户的列表,供用户从中进行选择。.login
- 要求用户在 Web 视图中进行身份验证。 如果指定此值,则一次只能将一个帐户登录。.consent
- 要求用户同意请求的当前范围集。
以无提示方式获取令牌
若要以无提示方式获取更新的令牌,请向 ViewController
类添加以下代码。 它创建 MSALSilentTokenParameters
对象并调用 acquireTokenSilent()
:
func acquireTokenSilently(_ account : MSALAccount!) {
guard let applicationContext = self.applicationContext else { return }
/**
Acquire a token for an existing account silently
- forScopes: Permissions you want included in the access token received
in the result in the completionBlock. Not all scopes are
guaranteed to be included in the access token returned.
- account: An account object that we retrieved from the application object before that the
authentication flow will be locked down to.
- completionBlock: The completion block that will be called when the authentication
flow completes, or encounters an error.
*/
let parameters = MSALSilentTokenParameters(scopes: kScopes, account: account)
applicationContext.acquireTokenSilent(with: parameters) { (result, error) in
if let error = error {
let nsError = error as NSError
// interactionRequired means we need to ask the user to sign-in. This usually happens
// when the user's Refresh Token is expired or if the user has changed their password
// among other possible reasons.
if (nsError.domain == MSALErrorDomain) {
if (nsError.code == MSALError.interactionRequired.rawValue) {
DispatchQueue.main.async {
self.acquireTokenInteractively()
}
return
}
}
self.updateLogging(text: "Could not acquire token silently: \(error)")
return
}
guard let result = result else {
self.updateLogging(text: "Could not acquire token: No result returned")
return
}
self.accessToken = result.accessToken
self.updateLogging(text: "Refreshed Access token is \(self.accessToken)")
self.updateSignOutButton(enabled: true)
self.getContentWithToken()
}
}
调用 Microsoft Graph API
在你获得令牌后,你的应用可以在 HTTP 标头中使用它向 Microsoft Graph 发出经授权的请求:
标头密钥 | 值 |
---|---|
授权 | 持有者 <access-token> |
将以下代码添加到 ViewController
类:
func getContentWithToken() {
// Specify the Graph API endpoint
let graphURI = getGraphEndpoint()
let url = URL(string: graphURI)
var request = URLRequest(url: url!)
// Set the Authorization header for the request. We use Bearer tokens, so we specify Bearer + the token we got from the result
request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
self.updateLogging(text: "Couldn't get graph result: \(error)")
return
}
guard let result = try? JSONSerialization.jsonObject(with: data!, options: []) else {
self.updateLogging(text: "Couldn't deserialize result JSON")
return
}
self.updateLogging(text: "Result from Graph: \(result))")
}.resume()
}
请参阅 Microsoft Graph API,了解有关 Microsoft Graph API 的详细信息。
使用 MSAL 进行注销
接下来,添加注销支持。
重要
用 MSAL 注销会从应用程序中删除有关用户的所有已知信息,同时会删除其设备上的获得设备配置允许的活动会话。 还可以选择性地从浏览器注销用户。
若要添加注销功能,请将以下代码添加到 ViewController
类中。
@objc func signOut(_ sender: AnyObject) {
guard let applicationContext = self.applicationContext else { return }
guard let account = self.currentAccount else { return }
do {
/**
Removes all tokens from the cache for this application for the provided account
- account: The account to remove from the cache
*/
let signoutParameters = MSALSignoutParameters(webviewParameters: self.webViewParameters!)
signoutParameters.signoutFromBrowser = false // set this to true if you also want to signout from browser or webview
applicationContext.signout(with: account, signoutParameters: signoutParameters, completionBlock: {(success, error) in
if let error = error {
self.updateLogging(text: "Couldn't sign out account with error: \(error)")
return
}
self.updateLogging(text: "Sign out completed successfully")
self.accessToken = ""
self.updateCurrentAccount(account: nil)
})
}
}
启用令牌缓存
默认情况下,MSAL 会在 iOS 或 macOS 密钥链中缓存应用的令牌。
若要启用令牌缓存,请执行以下操作:
- 确保应用程序已正确签名
- 转到 Xcode 项目设置 >“功能”选项卡>“启用密钥链共享”
- 选择 + 并输入以下 密钥链组之一:
- iOS:
com.microsoft.adalcache
- macOS:
com.microsoft.identity.universalstorage
- iOS:
添加帮助程序方法
将以下帮助程序方法添加到 ViewController
类以完成此示例。
iOS UI:
func updateLogging(text : String) {
if Thread.isMainThread {
self.loggingText.text = text
} else {
DispatchQueue.main.async {
self.loggingText.text = text
}
}
}
func updateSignOutButton(enabled : Bool) {
if Thread.isMainThread {
self.signOutButton.isEnabled = enabled
} else {
DispatchQueue.main.async {
self.signOutButton.isEnabled = enabled
}
}
}
func updateAccountLabel() {
guard let currentAccount = self.currentAccount else {
self.usernameLabel.text = "Signed out"
return
}
self.usernameLabel.text = currentAccount.username
}
func updateCurrentAccount(account: MSALAccount?) {
self.currentAccount = account
self.updateAccountLabel()
self.updateSignOutButton(enabled: account != nil)
}
macOS UI:
func updateLogging(text : String) {
if Thread.isMainThread {
self.loggingText.string = text
} else {
DispatchQueue.main.async {
self.loggingText.string = text
}
}
}
func updateSignOutButton(enabled : Bool) {
if Thread.isMainThread {
self.signOutButton.isEnabled = enabled
} else {
DispatchQueue.main.async {
self.signOutButton.isEnabled = enabled
}
}
}
func updateAccountLabel() {
guard let currentAccount = self.currentAccount else {
self.usernameLabel.stringValue = "Signed out"
return
}
self.usernameLabel.stringValue = currentAccount.username ?? ""
self.usernameLabel.sizeToFit()
}
func updateCurrentAccount(account: MSALAccount?) {
self.currentAccount = account
self.updateAccountLabel()
self.updateSignOutButton(enabled: account != nil)
}
仅限 iOS:获取其他设备信息
使用以下代码读取当前设备配置,包括设备是否配置为共享设备:
@objc func getDeviceMode(_ sender: AnyObject) {
if #available(iOS 13.0, *) {
self.applicationContext?.getDeviceInformation(with: nil, completionBlock: { (deviceInformation, error) in
guard let deviceInfo = deviceInformation else {
self.updateLogging(text: "Device info not returned. Error: \(String(describing: error))")
return
}
let isSharedDevice = deviceInfo.deviceMode == .shared
let modeString = isSharedDevice ? "shared" : "private"
self.updateLogging(text: "Received device info. Device is in the \(modeString) mode.")
})
} else {
self.updateLogging(text: "Running on older iOS. GetDeviceInformation API is unavailable.")
}
}
多帐户应用程序
该应用针对单个帐户方案生成。 MSAL 还支持多帐户场景,但它需要更多的应用程序工作。 你需要创建 UI 来帮助用户选择他们想要对每个需要令牌的操作使用的帐户。 或者,应用可以实现一种启发式算法,通过查询 MSAL 中的所有帐户来选择要使用的帐户。 有关示例,请参阅 accountsFromDeviceForParameters:completionBlock:
API
测试应用程序
构建应用并将其部署到测试设备或模拟器。 你应能够登录并获取 Microsoft Entra ID 或个人 Microsoft 帐户的令牌。
用户首次登录你的应用时,Microsoft 标识都将提示他们同意所请求的权限。 虽然大多数用户都能够同意,但某些 Microsoft Entra 租户已禁用用户同意功能,这要求管理员代表所有用户同意。 要支持此场景,请注册应用的范围。
你登录后,此应用将显示从 Microsoft Graph /me
终结点返回的数据。
后续步骤
在我们的多部分场景系列中,详细了解如何构建可调用受保护 Web API 的移动应用。