适用于: 员工租户
外部租户(了解详细信息)
这是本教程系列中的第三个教程,指导你使用 Microsoft Entra ID 登录用户。
在开始之前,请使用此页面顶部的 “选择租户类型 选择器”来选择租户类型。 Microsoft Entra ID 提供两种租户配置:员工和外部。 员工租户配置适用于员工、内部应用和其他组织资源。 外部租户适用于面向客户的应用。
在本教程中,你将:
- 登录用户。
- 注销用户。
- 创建应用的 UI
先决条件
登录用户
有两个主要选项可用于使用适用于 iOS 的Microsoft身份验证库(MSAL)登录用户:以交互方式或无提示方式获取令牌。
若要以交互方式登录用户,请使用以下代码:
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() } }
promptType
的MSALInteractiveTokenParameters
属性会配置身份验证和同意提示行为。 支持以下值:.promptIfNecessary
(默认值)- 仅在必要时才提示用户。 SSO 体验取决于 Web 视图中是否存在 Cookie,此外还取决于帐户类型。 如果有多个用户已登录,则会提供账户选择界面。 这是默认行为。.selectAccount
- 如果未指定用户,则身份验证 Web 视图会显示当前已登录帐户的列表,供用户从中进行选择。.login
- 要求用户在 Web 视图中进行身份验证。 如果指定此值,则一次只能将一个帐户登录。.consent
- 要求用户同意请求的当前权限范围集。
若要以无提示方式登录用户,请使用以下代码:
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() } }
该方法
acquireTokenSilently
尝试以无提示方式获取现有 MSAL 帐户的访问令牌。 它使用applicationContext
来请求具有指定作用域的令牌。 如果发生错误,它将检查是否需要用户交互,如果是,则启动交互式令牌获取。 成功后,它会更新访问令牌,记录结果,启用注销按钮,并使用令牌检索内容。
处理登录回调(仅限 iOS)
打开 AppDelegate.swift 文件。 若要在登录后处理回叫,请在 MSALPublicClientApplication.handleMSALResponse
类中添加 appDelegate
,如下所示:
// 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)
}
注销用户
重要
用 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)
})
}
}
创建应用的 UI
现在,请将以下代码添加到 ViewController
类,以便创建一个 UI,其中包含用于调用 Microsoft Graph API 的按钮,用于退出登录的按钮,以及用于查看某些输出的文本视图:
iOS 用户界面
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 用户界面
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()
}
后续步骤
这是本教程系列中的第三个教程,指导你使用 Microsoft Entra ID 登录用户。
在开始之前,请使用此页面顶部的 “选择租户类型 选择器”来选择租户类型。 Microsoft Entra ID 提供两种租户配置:员工和外部。 员工租户配置适用于员工、内部应用和其他组织资源。 外部租户适用于面向客户的应用。
在本教程中,你将:
- 登录用户。
- 注销用户。
先决条件
登录用户
有两个主要选项可用于使用适用于 iOS 的Microsoft身份验证库(MSAL)登录用户:以交互方式或无提示方式获取令牌。
若要以交互方式登录用户,请使用以下代码:
acquireTokenInteractively() { guard let applicationContext = self.applicationContext else { return } guard let webViewParameters = self.webViewParameters else { return } updateLogging(text: "Acquiring token interactively...") let parameters = MSALInteractiveTokenParameters(scopes: Configuration.kScopes, webviewParameters: webViewParameters) parameters.promptType = .selectAccount applicationContext.acquireToken(with: parameters) { (result, error) in 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 } self.accessToken = result.accessToken self.updateLogging(text: "Access token is \(self.accessToken)") self.updateCurrentAccount(account: result.account) } }
代码首先检查应用程序上下文和 Web 视图参数是否可用。 然后,它会更新日志记录,以指示它以交互方式获取令牌。 接下来,它设置用于交互式令牌获取的参数,并指定范围和 Web 视图参数。 它还将提示类型设置为选择帐户。
之后,它会使用定义的参数在应用程序上下文中调用
acquireToken
该方法。 在完成处理程序中,它会检查是否存在任何错误。 如果遇到错误,它会使用错误消息更新日志记录。 如果成功,它将从结果中检索访问令牌,使用令牌更新日志记录,并更新当前帐户。应用获取访问令牌后,可以检索与当前帐户关联的声明。 为此,请使用以下代码片段:
let claims = result.account.accountClaims let preferredUsername = claims?["preferred_username"] as? String
该代码通过访问
accountClaims
对象的属性result.account
从帐户读取声明。 然后,它会从声明字典中检索“preferred_username”声明的值,并将其分配给变量preferredUsername
。若要以无提示方式登录用户,请使用以下代码:
func acquireTokenSilently() { self.loadCurrentAccount { (account) in guard let currentAccount = account else { self.updateLogging(text: "No token found, try to acquire a token interactively first") return } self.acquireTokenSilently(currentAccount) } }
代码以无提示方式启动获取令牌的过程。 它首先尝试加载当前帐户。 如果找到当前帐户,它将继续使用该帐户以无提示方式获取令牌。 如果未找到当前帐户,它会更新日志记录以指示找不到令牌,并建议先尝试以交互方式获取令牌。
在上面的代码中,我们调用了两个函数,
loadCurrentAccount
以及acquireTokenSilently
。 该loadCurrentAccount
函数应具有以下代码:func loadCurrentAccount(completion: AccountCompletion? = nil) { guard let applicationContext = self.applicationContext else { return } let msalParameters = MSALParameters() msalParameters.completionBlockQueue = DispatchQueue.main // Note that this sample showcases an app that signs in a single account at a time 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.updateCurrentAccount(account: currentAccount) self.acquireTokenSilently(currentAccount) if let completion = completion { completion(self.currentAccount) } return } // If testing with Microsoft's shared device mode, see the account that has been signed out from another app. More details here: // https://docs.microsoft.com/azure/active-directory/develop/msal-ios-shared-devices if let previousAccount = previousAccount { self.updateLogging(text: "The account with username \(String(describing: previousAccount.username)) has been signed out.") } else { self.updateLogging(text: "") } self.accessToken = "" self.updateCurrentAccount(account: nil) if let completion = completion { completion(nil) } }) }
该代码使用适用于 iOS 的 MSAL 来加载当前帐户。 它会检查错误并相应地更新日志记录。 如果找到当前帐户,它会更新它,并尝试以无提示方式获取令牌。 如果存在以前的帐户,则会记录注销。如果未找到任何帐户,则会清除访问令牌。 最后,如果提供了完成块,它将执行该块。
该
acquireTokenSilently
函数应包含以下代码: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. */ updateLogging(text: "Acquiring token silently...") let parameters = MSALSilentTokenParameters(scopes: Configuration.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) } }
此函数使用适用于 iOS 的 MSAL 以无提示方式获取现有帐户的令牌。 验证
applicationContext
后,它会记录令牌获取过程。 使用MSALSilentTokenParameters
时,它定义必要的参数。 然后,它会尝试以无提示方式获取令牌。 如果出现错误,它会检查用户交互要求,并在需要时启动交互式进程。 成功后,它会更新accessToken
属性并记录刷新的令牌,最后启用注销按钮。
注销用户
若要使用适用于 iOS 的 MSAL 从 iOS (Swift) 应用注销用户,请使用以下代码:
@IBAction func signOut(_ sender: UIButton) {
guard let applicationContext = self.applicationContext else { return }
guard let account = self.currentAccount else { return }
guard let webViewParameters = self.webViewParameters else { return }
updateLogging(text: "Signing out...")
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: webViewParameters)
// If testing with Microsoft's shared device mode, trigger signout from browser. More details here:
// https://docs.microsoft.com/azure/active-directory/develop/msal-ios-shared-devices
if (self.currentDeviceMode == .shared) {
signoutParameters.signoutFromBrowser = true
} else {
signoutParameters.signoutFromBrowser = false
}
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)
})
}
}
代码验证是否存在 applicationContext
, currentAccount
以及 webViewParameters
。 然后,它会记录注销过程。 该代码从提供的帐户的缓存中删除所有令牌。 根据当前设备模式,系统会决定是否从浏览器退出登录。 完成后,它会相应地更新日志记录文本。 如果在注销过程中发生错误,则会记录错误消息。 成功注销后,它会将访问令牌更新为空字符串并清除当前帐户。