如何使用OAuth2与Swift实现自定义Web服务身份认证及数据获取?
Got it, let's walk through how to handle authentication for your custom web service and fetch JSON data afterward. Token-based authentication (like Bearer Tokens) is the most common approach for this scenario, so I'll use that as the example. I'll break down the flow and share a complete Swift implementation using modern async/await (works on iOS 15+, macOS 12+, etc.).
First, let's clarify the typical steps you'll need to follow:
- Send a request to the service's auth endpoint (usually a POST request) with your credentials (like username/password, API key, etc.).
- If authentication succeeds, the service will return an access token (often a JWT).
- Store this token securely (never use UserDefaults—use Keychain instead).
- For every subsequent request to fetch JSON data, include this token in the
AuthorizationHTTP header (formatted asBearer <your-token>). - Handle token expiration (many services return a refresh token to get a new access token without re-authenticating).
Step 1: Define Data Models
First, create Codable models to parse the auth response and your target JSON data:
import Foundation // Model for the authentication token response struct AuthToken: Codable { let accessToken: String let refreshToken: String? // Optional if your service doesn't use refresh tokens let expiresIn: TimeInterval // Seconds until token expires } // Example model for the data you want to fetch (customize this to match your service's JSON) struct UserData: Codable { let id: Int let name: String let email: String let profileData: [String: String] } // Custom error type to handle auth and data fetch issues enum ServiceError: Error, LocalizedError { case invalidCredentials case tokenFetchFailed case dataFetchFailed case invalidResponse case decodingError(String) case networkError(Error) var errorDescription: String? { switch self { case .invalidCredentials: return "Invalid username or password" case .tokenFetchFailed: return "Failed to retrieve authentication token" case .dataFetchFailed: return "Failed to fetch requested data" case .invalidResponse: return "Received invalid response from server" case .decodingError(let message): return "Decoding error: \(message)" case .networkError(let error): return "Network error: \(error.localizedDescription)" } } }
Step 2: Create a Service Manager Class
This class will handle authentication, token storage, and data fetching. I'll include Keychain storage for the token (using the Security framework):
import Security class CustomWebServiceManager { static let shared = CustomWebServiceManager() private let authEndpoint = "https://your-custom-service.com/api/auth/login" private let dataEndpoint = "https://your-custom-service.com/api/user/data" // MARK: - Token Storage (Keychain) private func saveTokenToKeychain(_ token: String) { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrAccount: "CustomServiceAccessToken", kSecValueData: token.data(using: .utf8)!, kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked ] // Delete existing token first to avoid duplicates SecItemDelete(query as CFDictionary) SecItemAdd(query as CFDictionary, nil) } private func getTokenFromKeychain() -> String? { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrAccount: "CustomServiceAccessToken", kSecReturnData: kCFBooleanTrue!, kSecMatchLimit: kSecMatchLimitOne ] var data: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &data) if status == errSecSuccess, let tokenData = data as? Data { return String(data: tokenData, encoding: .utf8) } return nil } // MARK: - Authentication func authenticate(username: String, password: String) async throws -> AuthToken { guard let url = URL(string: authEndpoint) else { throw ServiceError.tokenFetchFailed } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") // Prepare credentials body (adjust this to match your service's expected format) let credentials = ["username": username, "password": password] request.httpBody = try JSONSerialization.data(withJSONObject: credentials) do { let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw ServiceError.invalidResponse } // Handle HTTP status codes if httpResponse.statusCode == 401 { throw ServiceError.invalidCredentials } guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else { throw ServiceError.tokenFetchFailed } // Decode the token response let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase // Use if your service returns snake_case keys let token = try decoder.decode(AuthToken.self, from: data) // Save token to Keychain saveTokenToKeychain(token.accessToken) return token } catch { throw ServiceError.networkError(error) } } // MARK: - Fetch Protected Data func fetchProtectedData() async throws -> UserData { guard let token = getTokenFromKeychain() else { throw ServiceError.tokenFetchFailed } guard let url = URL(string: dataEndpoint) else { throw ServiceError.dataFetchFailed } var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") do { let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw ServiceError.invalidResponse } // Handle token expiration (401 means we need to re-authenticate or refresh) if httpResponse.statusCode == 401 { // Here you could trigger a token refresh flow, or throw an error to prompt re-login throw ServiceError.invalidCredentials } guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else { throw ServiceError.dataFetchFailed } // Decode the target data let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let userData = try decoder.decode(UserData.self, from: data) return userData } catch let decodingError as DecodingError { throw ServiceError.decodingError(decodingError.localizedDescription) } catch { throw ServiceError.networkError(error) } } }
Step 3: Usage Example
You can call these methods from a view model or view (using SwiftUI as an example):
import SwiftUI class DataViewModel: ObservableObject { @Published var userData: UserData? @Published var errorMessage: String? func loginAndFetchData(username: String, password: String) { Task { do { // First authenticate let token = try await CustomWebServiceManager.shared.authenticate(username: username, password: password) print("Token received: \(token.accessToken)") // Then fetch protected data let data = try await CustomWebServiceManager.shared.fetchProtectedData() DispatchQueue.main.async { self.userData = data self.errorMessage = nil } } catch let error as ServiceError { DispatchQueue.main.async { self.errorMessage = error.localizedDescription } } catch { DispatchQueue.main.async { self.errorMessage = "Unexpected error: \(error.localizedDescription)" } } } } } // Example SwiftUI View struct ContentView: View { @StateObject private var viewModel = DataViewModel() @State private var username = "" @State private var password = "" var body: some View { VStack(spacing: 20) { TextField("Username", text: $username) .textFieldStyle(.roundedBorder) SecureField("Password", text: $password) .textFieldStyle(.roundedBorder) Button("Login & Fetch Data") { viewModel.loginAndFetchData(username: username, password: password) } if let errorMessage = viewModel.errorMessage { Text(errorMessage) .foregroundColor(.red) } if let userData = viewModel.userData { Text("Welcome, \(userData.name)!") Text("Email: \(userData.email)") // Display other data as needed } } .padding() } }
- Token Security: Always use Keychain for storing tokens—UserDefaults is not secure (it's stored in plaintext).
- Token Refresh: If your service provides a refresh token, implement a refresh flow to get a new access token when the old one expires (instead of forcing the user to re-login).
- SSL Validation: Ensure your custom service uses HTTPS. For development, you might need to disable ATS (App Transport Security) temporarily, but never do this in production.
- Error Handling: The example includes a custom error type to make debugging easier—expand it to cover more edge cases specific to your service.
- Dependency Injection: For testability, consider injecting a URLSession instance into the service manager instead of using
URLSession.shared.
内容的提问来源于stack exchange,提问作者user5303808

