
Так или иначе, все реже можно найти приложение, которое не требует создания аккаунта для полноценной работы. В связи с этим возникает необходимость в некоторого рода защищенном хранилище аутентификационных данных. В 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 } }
Сформируем минимальные необходимые требования к защищенному хранилищу.
-
Возможность сохранить данные.
-
Возможность загрузить данные.
-
Возможность удалить данные.
В то же время сформулируем основные ограничения к коду.
-
Объект должен формировать четкую и понятную абстракцию. Причем уровень абстракции должен соблюдаться и внутри методов.
-
Объект должен быть инкапсулирован, предоставляя другим объектам лишь несколько методов для вызова, скрывая внутреннюю реализацию.
В результате работы над данными требованиями и ограничениями был разработан класс 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), либо, как приводится в примере, создать экземпляр по необходимости.
Заключение
Хранение авторизационных данных — довольно частая задача, которая решается разработчиками, поскольку токен авторизации через определенное время утрачивает актуальность. Для того, чтобы пользовательский опыт был максимально положительным, нужно выполнять повторную авторизацию в «тихом» режиме, а не выбрасывать пользователя на экран ввода логина и пароля.
В любом случае, это один из наиболее распространенных вариантов использования защищенного хранилища. В качестве другого примера можно привести хранение «секретов» пользователя. Но об этом как-нибудь в другой раз.
ссылка на оригинал статьи https://habr.com/ru/post/670490/
Добавить комментарий