Введение: зачем нужна авторизация в мобильных приложениях?
Современные iOS-приложения редко работают изолированно. Они общаются с серверами, отправляют данные, получают персонализированный контент. Но как серверу понять, что запрос отправлен именно тем пользователем, за которого он себя выдает? Здесь на помощь приходят OAuth 2.0 и JWT (JSON Web Token). Это не просто модные аббревиатуры, а стандарты безопасности, которые лежат в основе работы большинства современных API (Google, Facebook, GitHub, ваш собственный бэкенд).
В этой статье мы разберем, как работают OAuth 2.0 и JWT на Swift. Вы узнаете, чем они отличаются, как их правильно реализовать и какие подводные камни ждут новичка. Мы напишем код для получения, хранения и обновления токенов. Поехали!
1. Основы: OAuth 2.0 и JWT — в чем разница?
Многие путают эти два понятия. На самом деле, это разные уровни абстракции.
- OAuth 2.0 — это протокол авторизации. Он описывает, как приложение (клиент) может получить доступ к ресурсам пользователя на стороннем сервере (например, к его фотографиям в Google) без передачи логина и пароля. Главная идея — выдача временных токенов доступа.
- JWT — это формат токена. Это компактная, URL-безопасная строка, которая содержит JSON-данные (например, id пользователя, роль, срок действия) и подпись. JWT часто используется как access token в OAuth 2.0, но может применяться и отдельно.
Простая аналогия: OAuth 2.0 — это правила выдачи пропуска на стадион, а JWT — сам пластиковый пропуск с вашей фотографией и печатью.
В типичном сценарии iOS-приложение:
- Открывает браузер для входа через OAuth-провайдера (Google, Apple ID, свой сервер).
- Получает authorization code.
- Обменивает код на access token (часто в формате JWT) и refresh token.
- Использует access token для запросов к API.
- Когда access token истекает, использует refresh token для получения нового.
2. Структура JWT токена
JWT состоит из трех частей, разделенных точками:
header.payload.signature
Пример декодированного JWT:
// Header (заголовок)
{
"alg": "HS256",
"typ": "JWT"
}
// Payload (полезная нагрузка)
{
"sub": "1234567890",
"name": "Иван Петров",
"iat": 1516239022,
"exp": 1516242622
}
// Signature (подпись) - создается сервером
Важно: Payload JWT не зашифрован, он только закодирован в Base64. Любой может его прочитать. Секретность обеспечивает подпись — если сервер видит, что подпись не совпадает, он отклоняет токен. Поэтому никогда не храните пароли или секретные данные в JWT.
3. Реализация OAuth 2.0 и JWT на Swift
Теперь перейдем к практике. Мы напишем класс для работы с авторизацией. Для простоты будем использовать встроенный URLSession, хотя в реальных проектах часто используют Alamofire или SwiftUI + async/await.
3.1. Модели данных
struct AuthResponse: Codable {
let accessToken: String
let refreshToken: String?
let expiresIn: Int
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case refreshToken = "refresh_token"
case expiresIn = "expires_in"
}
}
struct UserProfile: Codable {
let id: String
let name: String
let email: String
}
3.2. Сервис авторизации
Создадим класс AuthService, который будет отвечать за получение и обновление токенов. Обратите внимание: в реальном приложении URL и clientSecret должны храниться безопасно (например, в Keychain или через сервер конфигурации).
class AuthService {
static let shared = AuthService()
private let baseURL = "https://yourapi.com"
private let clientID = "YOUR_CLIENT_ID"
private let clientSecret = "YOUR_CLIENT_SECRET"
// MARK: - Получение токена по коду авторизации
func exchangeCodeForToken(code: String, completion: @escaping (Result<AuthResponse, Error>) -> Void) {
let url = URL(string: "\(baseURL)/oauth/token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: String] = [
"grant_type": "authorization_code",
"code": code,
"client_id": clientID,
"client_secret": clientSecret,
"redirect_uri": "yourapp://callback"
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error ?? NetworkError.unknown))
return
}
do {
let authResponse = try JSONDecoder().decode(AuthResponse.self, from: data)
self.saveTokens(authResponse)
completion(.success(authResponse))
} catch {
completion(.failure(error))
}
}.resume()
}
// MARK: - Обновление токена
func refreshAccessToken(completion: @escaping (Result<AuthResponse, Error>) -> Void) {
guard let refreshToken = loadRefreshToken() else {
completion(.failure(NetworkError.noRefreshToken))
return
}
let url = URL(string: "\(baseURL)/oauth/token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: String] = [
"grant_type": "refresh_token",
"refresh_token": refreshToken,
"client_id": clientID,
"client_secret": clientSecret
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error ?? NetworkError.unknown))
return
}
do {
let authResponse = try JSONDecoder().decode(AuthResponse.self, from: data)
self.saveTokens