You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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.).

1. Core Authentication Flow

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 Authorization HTTP header (formatted as Bearer <your-token>).
  • Handle token expiration (many services return a refresh token to get a new access token without re-authenticating).
2. Full Swift Implementation

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()
    }
}
3. Key Notes & Enhancements
  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 08:48:52