Поделиться через


Руководство: Вход пользователей в мобильное приложение iOS (Swift)

Применяется к: зеленый круг с символом белой галочки, указывающим следующее содержимое, применимое к клиентам рабочей силы. Клиенты рабочей силы зеленый круг с символом белой галочки, указывающий на следующее содержимое, применимое к внешним клиентам. Внешние клиенты (дополнительные сведения)

Это третий учебник в серии учебников, который поможет вам выполнить вход пользователей с помощью идентификатора Microsoft Entra.

Прежде чем начать, используйте селектор Выбрать тип арендатора в верхней части этой страницы, чтобы выбрать тип арендатора. Microsoft Entra ID предоставляет два варианта конфигурации клиента: для рабочих нужд и для внешних. Конфигурация клиента для сотрудников, внутренних приложений и других организационных ресурсов. Внешний клиент предназначен для ваших клиентских приложений.

В этом руководстве вы:

  • Войдите как пользователь.
  • Выйти из учетной записи пользователя.
  • Создание пользовательского интерфейса приложения

Необходимые условия

  • Руководство: подготовка приложения iOS (Swift) для проверки подлинности.

Вход пользователя

У вас есть два основных варианта авторизации пользователей с помощью библиотеки подлинности Майкрософт (MSAL) для iOS: интерактивное или незаметное получение токенов.

  1. Для интерактивного входа в систему используйте следующий код:

    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 определяется наличием файлов cookie в веб-интерфейсе и типом учетной записи. При входе нескольких пользователей отображается интерфейс выбора учетной записи. Это поведение по умолчанию.
    • .selectAccount - Если пользователь не указан, веб-форма аутентификации покажет список учетных записей, которые уже вошли в систему, из которых пользователь может выбрать.
    • .login. Требуется, чтобы пользователь прошел проверку подлинности в веб-представлении. При указании этого значения можно входить только в одну учетную запись одновременно.
    • .consent. Требуется, чтобы пользователь согласился с текущим набором областей для запроса.
  2. Чтобы выполнить тихий вход для пользователя, используйте следующий код:

    
        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)
            })

        }
    }

Создание пользовательского интерфейса приложения

Теперь создайте пользовательский интерфейс, который включает кнопку для вызова API Microsoft Graph, другой для выхода и текстового представления, чтобы просмотреть некоторые выходные данные, добавив следующий код в класс 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()
    }

Дальнейшие действия

Руководство: вызов защищенного веб-API в приложении iOS (Swift)

Это третий учебник в серии учебников, который поможет вам выполнить вход пользователей с помощью идентификатора Microsoft Entra.

Прежде чем начать, используйте селектор Выбрать тип арендатора в верхней части этой страницы, чтобы выбрать тип арендатора. Microsoft Entra ID предоставляет два варианта конфигурации клиента: для рабочих нужд и для внешних. Конфигурация клиента для сотрудников, внутренних приложений и других организационных ресурсов. Внешний клиент предназначен для ваших клиентских приложений.

В этом руководстве вы:

  • Войдите как пользователь.
  • Выйти из учетной записи пользователя.

Необходимые условия

  • Руководство: подготовка приложения iOS (Swift) для проверки подлинности.

Вход пользователя

У вас есть два основных варианта авторизации пользователей с помощью библиотеки подлинности Майкрософт (MSAL) для iOS: интерактивное или незаметное получение токенов.

  1. Для интерактивного входа в систему используйте следующий код:

    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)
        }
    }
    

    Код сначала проверяет, доступны ли параметры контекста приложения и веб-представления. Затем он обновляет логирование, чтобы указать, что токен получается в интерактивном режиме. Затем он устанавливает параметры для интерактивного приобретения токенов, указывая области действия и параметры веб-интерфейса. Он также задает тип запроса для выбора учетной записи.

    Затем он вызывает метод acquireToken в контексте приложения с определенными параметрами. В обработчике завершения он проверяет наличие ошибок. Если возникла ошибка, он обновляет журналирование с сообщением об ошибке. При успешном выполнении извлекается токен доступа из результата, обновляется логирование с использованием токена и обновляется текущая учетная запись.

    После получения токена доступа приложение сможет извлечь утверждения, связанные с текущей учетной записью. Для этого используйте следующий фрагмент кода:

    let claims = result.account.accountClaims
    let preferredUsername = claims?["preferred_username"] as? String
    

    Код считывает утверждения из учетной записи путем доступа к свойству accountClaims объекта result.account. Затем он извлекает значение утверждения "preferred_username" из словаря утверждений и назначает его переменной preferredUsername.

  2. Чтобы выполнить тихий вход для пользователя, используйте следующий код:

    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)
            }
        })
    }
    

    Код использует MSAL для iOS для загрузки текущей учетной записи. Он проверяет наличие ошибок и обновляет ведение журнала соответствующим образом. Если обнаружена текущая учетная запись, она обновляется и пытается в фоновом режиме получить токены. Если предыдущая учетная запись существует, то регистрируется выход. Если учетные записи не найдены, она очищает токен доступа. Наконец, он выполняет блок завершения, если он указан.

    Функция 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)
        }
    }
    
    

    Эта функция использует MSAL для iOS, чтобы незаметно получить токен для существующей учетной записи. После проверки applicationContext он логирует процесс приобретения токена. Используя MSALSilentTokenParameters, он определяет необходимые параметры. Затем он пытается получить токен незаметно. Если возникают ошибки, он проверяет требования взаимодействия с пользователем, инициируя интерактивный процесс при необходимости. После успешного завершения он обновляет свойство accessToken, логирует обновленный токен и завершает процесс, активируя кнопку выхода.

Выход из учётной записи пользователя

Чтобы вывести пользователя из приложения iOS (Swift) с помощью MSAL для iOS, используйте следующий код:

   @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. Затем он регистрирует процесс выхода. Код удаляет все маркеры из кэша для предоставленной учетной записи. В зависимости от текущего режима устройства определяется, следует ли выйти из браузера. По завершении он обновляет текст ведения журнала соответствующим образом. Если во время процесса выхода возникает ошибка, он регистрирует сообщение об ошибке. После успешного выхода он обновляет маркер доступа до пустой строки и очищает текущую учетную запись.

Дальнейшие действия

Руководство: вызов защищенного веб-API в приложении iOS (Swift)