KeyChain. Аккаунт пользователя

от автора

Так или иначе, все реже можно найти приложение, которое не требует создания аккаунта для полноценной работы. В связи с этим возникает необходимость в некоторого рода защищенном хранилище аутентификационных данных. В iOS для этих целей используется framework Security и его сервис KeyChain. Далее будет описан подход для работы с этим сервисом.

Данные пользователя

Как сказано в документации, хранилище используется для безопасного хранения небольших объемов данных. Поэтому требуется некоторый объект Credentials, содержащий информацию об аккаунте, и с которым впоследствии будет происходить работа.

public struct Credentials { public var account: String public var server: String public var password: String?  public init(account: String, server: String, password: String? = nil) { self.account = account self.server = server self.password = password } }

Сформируем минимальные необходимые требования к защищенному хранилищу.

  1. Возможность сохранить данные.

  2. Возможность загрузить данные.

  3. Возможность удалить данные.

В то же время сформулируем основные ограничения к коду.

  1. Объект должен формировать четкую и понятную абстракцию. Причем уровень абстракции должен соблюдаться и внутри методов.

  2. Объект должен быть инкапсулирован, предоставляя другим объектам лишь несколько методов для вызова, скрывая внутреннюю реализацию.

В результате работы над данными требованиями и ограничениями был разработан класс KeyChain.

import Foundation import Security  public final class KeyChain {  // MARK: - Types  public struct Credentials { public var account: String public var server: String public var password: String?  public init(account: String, server: String, password: String? = nil) { self.account = account self.server = server self.password = password } }  public enum Error: Swift.Error { case encodingError case decodingError case errorStatus(message: String?) }  // MARK: - Lifecycle  public init() {}  // MARK: - Methods  public func save(credentials: Credentials) throws { let query = query(from: credentials)  if let password = credentials.password { try setValue(password, query: query) } else { try remove(credentials: credentials) } }  public func load(credentials: inout Credentials) throws { var query = query(from: credentials) query[kSecMatchLimit as String] = kSecMatchLimitOne query[kSecReturnAttributes as String] = kCFBooleanTrue query[kSecReturnData as String] = kCFBooleanTrue  credentials.password = try getValue(query: query) }  public func remove(credentials: Credentials) throws { let query = query(from: credentials) try delete(query: query) }  private func setValue(_ value: String, query: [String: Any]) throws { guard let data = value.data(using: .utf8) else { throw Error.encodingError }  let status = SecItemCopyMatching(query as CFDictionary, nil)  switch status { case errSecSuccess: var attributesToUpdate: [String: Any] = [:] attributesToUpdate[kSecValueData as String] = data  try update(query: query, attributesToUpdate: attributesToUpdate) case errSecItemNotFound: var query = query query[kSecValueData as String] = data  try add(query: query) default: throw error(from: status) } }  private func getValue(query: [String: Any]) throws -> String? { var searchResult: AnyObject?  let status = withUnsafeMutablePointer(to: &searchResult) { SecItemCopyMatching(query as CFDictionary, $0) }  switch status { case errSecSuccess: guard let queriedItem = searchResult as? [String: Any],   let data = queriedItem[kSecValueData as String] as? Data,   let value = String(data: data, encoding: .utf8) else { throw Error.decodingError }  return value case errSecItemNotFound: return nil default: throw error(from: status) } }  private func update(query: [String: Any], attributesToUpdate: [String: Any]) throws { let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)  if status != errSecSuccess { throw error(from: status) } }  private func add(query: [String: Any]) throws { let status = SecItemAdd(query as CFDictionary, nil)  if status != errSecSuccess { throw error(from: status) } }  private func delete(query: [String: Any]) throws { let status = SecItemDelete(query as CFDictionary)  if !(status == errSecSuccess || status == errSecItemNotFound) { throw error(from: status) } }  private func query(from credentials: Credentials) -> [String: Any] { var query: [String: Any] = [:] query[kSecClass as String] = kSecClassInternetPassword query[kSecAttrAccount as String] = credentials.account query[kSecAttrServer as String] = credentials.server return query }  private func error(from status: OSStatus) -> Error { let message = SecCopyErrorMessageString(status, nil) as String? return .errorStatus(message: message) } } 

Как можно убедиться, требования к хранилищу реализованы с помощью следующего интерфейса.

init() func save(credentials: Credentials) throws func load(credentials: inout Credentials) throws func remove(credentials: Credentials) throws

Все что касается ограничений, связанных с кодированием, опытный читатель может проанализировать самостоятельно.

Таким образом, при завершении срока действия токена авторизации и наличии в хранилище аккаунта пользователя, не будет необходимости в отображении экрана авторизации. Достаточно будет загрузить аккаунт из хранилища и авторизовать пользователя повторно.

class ViewController: UIViewController { private let secureStore = KeyChain()  override func viewDidLoad() { super.viewDidLoad()  var credentials = KeyChain.Credentials(account: "Test", server: "www.test.com")  do { try secureStore.load(credentials: &credentials) print(credentials) } catch { print(error) } } }

Причем можно повторить подход, который используется в Apple — объявить общий экземпляр (singleton), либо передать объект в окружение (environmentObject), либо, как приводится в примере, создать экземпляр по необходимости.

Заключение

Хранение авторизационных данных — довольно частая задача, которая решается разработчиками, поскольку токен авторизации через определенное время утрачивает актуальность. Для того, чтобы пользовательский опыт был максимально положительным, нужно выполнять повторную авторизацию в «тихом» режиме, а не выбрасывать пользователя на экран ввода логина и пароля.

В любом случае, это один из наиболее распространенных вариантов использования защищенного хранилища. В качестве другого примера можно привести хранение «секретов» пользователя. Но об этом как-нибудь в другой раз.

  1. Keychain Services

  2. Security


ссылка на оригинал статьи https://habr.com/ru/post/670490/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *