引言:为什么 OAuth 2.0 和 JWT 是 iOS 应用的标准
现代移动应用很少独立运行。它们中的大多数与远程 API 交互,需要安全的用户身份验证和授权。在 Apple 生态系统(iOS、macOS、watchOS)中,OAuth 2.0(访问委托协议)和 JWT(JSON Web Token——一种紧凑的令牌格式)的组合已成为事实上的标准。这种组合提供了灵活性、安全性和集成便利性。
在本文中,我们不会深入探讨枯燥的理论。相反,我们将分析 Swift 中的实际实现。您将了解如何通过 OAuth 2.0 获取访问令牌,如何将其安全地存储在 Keychain 中,如何在客户端解码 JWT,以及如何无需用户干预地刷新令牌。所有代码示例都可用于实际项目。
1. Swift 中的 OAuth 2.0 架构:从请求到令牌
首先,我们确定流程。在移动应用中,最常使用的是 授权码流程与 PKCE(Proof Key for Code Exchange)。这是公共客户端最安全的选择(移动应用无法存储密钥)。
让我们创建一个用于 OAuth 配置的结构体:
struct OAuthConfig { let clientID: String let redirectURI: String let authorizationEndpoint: String let tokenEndpoint: String let scope: String}第一步是构建授权 URL。对于 PKCE,我们需要生成 codeVerifier 及其 SHA256 哈希值 codeChallenge:
import Foundationimport CommonCrypto
class PKCEGenerator { static func generateCodeVerifier() -> String { var buffer = [UInt8](repeating: 0, count: 32) _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer) return Data(buffer).base64URLEncodedString() } static func generateCodeChallenge(from verifier: String) -> String { guard let data = verifier.data(using: .utf8) else { return "" } var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) data.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) } return Data(hash).base64URLEncodedString() }}
extension Data { func base64URLEncodedString() -> String { return self.base64EncodedString() .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "=", with: "") }}现在,我们构建用于在浏览器中打开的 URL(通过 ASWebAuthenticationSession):
func buildAuthorizationURL(config: OAuthConfig, codeChallenge: String) -> URL? { var components = URLComponents(string: config.authorizationEndpoint) components?.queryItems = [ URLQueryItem(name: "response_type", value: "code"), URLQueryItem(name: "client_id", value: config.clientID), URLQueryItem(name: "redirect_uri", value: config.redirectURI), URLQueryItem(name: "scope", value: config.scope), URLQueryItem(name: "code_challenge", value: codeChallenge), URLQueryItem(name: "code_challenge_method", value: "S256") ] return components?.url}授权成功后,用户将被重定向到带有 code 参数的 redirectURI。此代码用于交换令牌:
func exchangeCodeForToken(config: OAuthConfig, code: String, codeVerifier: String) async throws -> TokenResponse { let url = URL(string: config.tokenEndpoint)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") let body = "grant_type=authorization_code" + "&co