適用於:
員工租戶
外部租用戶 (深入瞭解)
這是教學課程系列中的第三個教學課程,會引導您使用 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 體驗取決於 Webview 中是否有 Cookie,以及帳戶類型。 如果有多個使用者登入,則會顯示帳戶選擇體驗。 這是預設行為。 -
.selectAccount- 如果未指定任何使用者,驗證 Webview 會顯示目前已登入的帳戶清單,供用戶選取。 -
.login- 要求使用者在 Webview 中驗證。 如果您指定此值,一次只能登入一個帳戶。 -
.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。 如果您支援 UISceneDelegate 和 UIApplicationDelegate 以與舊版 iOS 相容,則須在這兩個檔案中加入 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
現在,建立一個 UI,其中包含一個按鈕來呼叫 Microsoft Graph API、另一個按鈕用於登出,還有一個文字檢視框來查看部分輸出,並將下列程式碼新增至 ViewController 類別:
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是否存在。 然後,它會記錄註銷過程。 程序碼會從所提供帳戶的快取中移除所有記號。 根據目前的裝置模式,系統會決定是否要從瀏覽器簽出。 完成後,它會據以更新記錄文字。 如果在登出過程中發生錯誤,則會記錄錯誤訊息。 成功登出後,它會將存取令牌更新為空字串,並清除當前帳戶。