import Foundation /// Token管理器 /// 负责管理应用的认证令牌,包括验证、刷新和过期处理 class TokenManager { /// 单例实例 static let shared = TokenManager() /// token有效期阈值(秒),在token即将过期前进行刷新 /// 例如:设置为300表示在token过期前5分钟开始刷新 private let tokenValidityThreshold: TimeInterval = 300 /// 私有化初始化方法,确保单例模式 private init() {} // MARK: - Token 状态检查 /// 检查是否存在有效的访问令牌 var hasToken: Bool { return KeychainHelper.getAccessToken()?.isEmpty == false } // MARK: - Token 验证 /// 验证并刷新token(如果需要) /// - 检查token是否存在 /// - 检查token是否有效 /// - 在token即将过期时自动刷新 /// - Parameter completion: 完成回调,返回验证/刷新结果 /// - isValid: token是否有效 /// - error: 错误信息(如果有) func validateAndRefreshTokenIfNeeded(completion: @escaping (Bool, Error?) -> Void) { // 1. 检查token是否存在 guard let token = KeychainHelper.getAccessToken(), !token.isEmpty else { // token不存在,返回未授权错误 let error = NSError( domain: "TokenManager", code: 401, userInfo: [NSLocalizedDescriptionKey: "未找到访问令牌"] ) completion(false, error) return } // 2. 检查token是否有效 if isTokenValid(token) { // token有效,直接返回成功 completion(true, nil) return } // 3. token无效或即将过期,尝试刷新 refreshToken { [weak self] success, error in if success { // 刷新成功,返回成功 completion(true, nil) } else { // 刷新失败,返回错误信息 let finalError = error ?? NSError( domain: "TokenManager", code: 401, userInfo: [NSLocalizedDescriptionKey: "Token刷新失败"] ) completion(false, finalError) } } } /// 检查token是否有效 /// - Parameter token: 要检查的token字符串 /// - Returns: 如果token有效返回true,否则返回false /// /// 该方法会检查token的有效性,包括检查token是否为空、是否过期以及通过网络请求验证token。 /// /// - Note: 该方法会打印一些调试信息,包括token验证开始、token过期时间等。 public func isTokenValid(_ token: String) -> Bool { print("🔍 TokenManager: 开始验证token...") // 1. 基础验证:检查token是否为空 guard !token.isEmpty else { print("❌ TokenManager: Token为空") return false } // 2. 检查token是否过期(如果可能) if let expiryDate = getTokenExpiryDate(token) { print("⏰ TokenManager: Token过期时间: \(expiryDate)") if Date() > expiryDate { print("❌ TokenManager: Token已过期") return false } } // 3. 创建信号量用于同步网络请求 let semaphore = DispatchSemaphore(value: 0) var isValid = false var requestCompleted = false print("🌐 TokenManager: 发送验证请求到服务器...") // 4. 发送验证请求 let task = URLSession.shared.dataTask(with: createValidationRequest(token: token)) { data, response, error in defer { requestCompleted = true semaphore.signal() } // 检查网络错误 if let error = error { print("❌ TokenManager: 验证请求错误: \(error.localizedDescription)") return } // 检查响应状态码 guard let httpResponse = response as? HTTPURLResponse else { print("❌ TokenManager: 无效的服务器响应") return } print("📡 TokenManager: 服务器响应状态码: \(httpResponse.statusCode)") // 检查状态码 guard (200...299).contains(httpResponse.statusCode) else { print("❌ TokenManager: 服务器返回错误状态码: \(httpResponse.statusCode)") return } // 检查是否有数据 if let data = data, !data.isEmpty { do { // 尝试解析响应数据 let response = try JSONDecoder().decode(IdentityCheckResponse.self, from: data) isValid = response.isValid print("✅ TokenManager: Token验证\(isValid ? "成功" : "失败")") } catch { print("❌ TokenManager: 解析响应数据失败: \(error.localizedDescription)") // 如果解析失败但状态码是200,我们假设token是有效的 isValid = true print("ℹ️ TokenManager: 状态码200,假设token有效") } } else { // 如果没有返回数据但状态码是200,我们假设token是有效的 print("ℹ️ TokenManager: 没有返回数据,但状态码为200,假设token有效") isValid = true } } task.resume() // 5. 设置超时时间(10秒) let timeoutResult = semaphore.wait(timeout: .now() + 15) // 检查是否超时 if !requestCompleted && timeoutResult == .timedOut { print("⚠️ TokenManager: 验证请求超时") task.cancel() return false } return isValid } /// 创建验证请求 private func createValidationRequest(token: String) -> URLRequest { let url = URL(string: APIConfig.baseURL + "/iam/identity-check")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") return request } /// 从token中提取过期时间(示例实现) private func getTokenExpiryDate(_ token: String) -> Date? { // 这里需要根据实际的JWT或其他token格式来解析过期时间 // 以下是JWT token的示例解析 let parts = token.components(separatedBy: ".") guard parts.count > 1, let payloadData = base64UrlDecode(parts[1]) else { return nil } do { if let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any], let exp = payload["exp"] as? TimeInterval { return Date(timeIntervalSince1970: exp) } } catch { print("❌ TokenManager: 解析token过期时间失败: \(error.localizedDescription)") } return nil } private func base64UrlDecode(_ base64Url: String) -> Data? { var base64 = base64Url .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") // 添加必要的填充 let length = Double(base64.lengthOfBytes(using: .utf8)) let requiredLength = 4 * ceil(length / 4.0) let paddingLength = requiredLength - length if paddingLength > 0 { let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) base64 += padding } return Data(base64Encoded: base64) } /// 刷新token /// - Parameter completion: 刷新完成回调 /// - success: 是否刷新成功 /// - error: 错误信息(如果有) private func refreshToken(completion: @escaping (Bool, Error?) -> Void) { // 获取刷新令牌 guard let refreshToken = KeychainHelper.getRefreshToken(), !refreshToken.isEmpty else { // 没有可用的刷新令牌 let error = NSError( domain: "TokenManager", code: 401, userInfo: [NSLocalizedDescriptionKey: "未找到刷新令牌"] ) completion(false, error) return } // 准备刷新请求参数 let parameters: [String: Any] = [ "refresh_token": refreshToken, "grant_type": "refresh_token" ] // 发送刷新请求 NetworkService.shared.postWithToken(path: "/v1/iam/access-token-refresh", parameters: parameters) { (result: Result) in switch result { case .success(let tokenResponse): // 1. 保存新的访问令牌 KeychainHelper.saveAccessToken(tokenResponse.accessToken) // 2. 如果返回了新的刷新令牌,也保存起来 if let newRefreshToken = tokenResponse.refreshToken { KeychainHelper.saveRefreshToken(newRefreshToken) } print("✅ Token刷新成功") completion(true, nil) case .failure(let error): print("❌ Token刷新失败: \(error.localizedDescription)") // 刷新失败,清除本地token,需要用户重新登录 KeychainHelper.clearTokens() completion(false, error) } } } /// 清除所有存储的 token func clearTokens() { print("🗑️ TokenManager: 清除所有 token") KeychainHelper.clearTokens() // 清除其他与 token 相关的存储 UserDefaults.standard.removeObject(forKey: "tokenExpiryDate") UserDefaults.standard.synchronize() } } // MARK: - Token响应模型 /// 用于解析token刷新接口的响应数据 private struct TokenResponse: Codable { /// 访问令牌 let accessToken: String /// 刷新令牌(可选) let refreshToken: String? /// 过期时间(秒) let expiresIn: TimeInterval? /// 令牌类型(如:Bearer) let tokenType: String? // 使用CodingKeys自定义键名映射 enum CodingKeys: String, CodingKey { case accessToken = "access_token" case refreshToken = "refresh_token" case expiresIn = "expires_in" case tokenType = "token_type" } } // MARK: - 身份验证响应模型 /// 用于解析身份验证接口的响应数据 private struct IdentityCheckResponse: Codable { /// 是否有效 let isValid: Bool /// 用户ID(可选) let userId: String? /// 过期时间(可选) let expiresAt: Date? enum CodingKeys: String, CodingKey { case isValid = "is_valid" case userId = "user_id" case expiresAt = "expires_at" } } // MARK: - NetworkService扩展 /// 为NetworkService添加带token验证的请求方法 extension NetworkService { // MARK: - 核心请求方法 /// 带token验证的网络请求 /// - Parameters: /// - method: HTTP方法(GET/POST/PUT/DELETE等) /// - path: 接口路径 /// - parameters: 请求参数 /// - headers: 自定义请求头 /// - completion: 完成回调 func requestWithToken( _ method: String, path: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping (Result) -> Void ) { // 1. 验证并刷新token TokenManager.shared.validateAndRefreshTokenIfNeeded { [weak self] isValid, error in guard let self = self else { return } if isValid { // 2. token有效,继续发送原始请求 switch method.uppercased() { case "GET": self.get(path: path, parameters: parameters, headers: headers, completion: completion) case "POST": self.post(path: path, parameters: parameters, headers: headers, completion: completion) case "PUT": self.put(path: path, parameters: parameters, headers: headers, completion: completion) case "DELETE": self.delete(path: path, parameters: parameters, headers: headers, completion: completion) default: let error = NSError( domain: "NetworkService", code: 400, userInfo: [NSLocalizedDescriptionKey: "不支持的HTTP方法: \(method)"] ) completion(.failure(.other(error))) } } else { // 3. token无效,返回未授权错误 let error = error ?? NSError( domain: "NetworkService", code: 401, userInfo: [NSLocalizedDescriptionKey: "未授权访问"] ) DispatchQueue.main.async { completion(.failure(.unauthorized)) } // 4. 发送登出通知,让应用处理未授权情况 NotificationCenter.default.post(name: .userDidLogout, object: nil) } } } // MARK: - 便捷方法 /// 带token验证的GET请求 func getWithToken( path: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping (Result) -> Void ) { requestWithToken("GET", path: path, parameters: parameters, headers: headers, completion: completion) } /// 带token验证的POST请求 func postWithToken( path: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping (Result) -> Void ) { requestWithToken("POST", path: path, parameters: parameters, headers: headers, completion: completion) } /// 带token验证的PUT请求 func putWithToken( path: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping (Result) -> Void ) { requestWithToken("PUT", path: path, parameters: parameters, headers: headers, completion: completion) } /// 带token验证的DELETE请求 func deleteWithToken( path: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping (Result) -> Void ) { requestWithToken("DELETE", path: path, parameters: parameters, headers: headers, completion: completion) } } // MARK: - 通知名称 /// 定义应用中使用的通知名称 extension Notification.Name { /// 用户登出通知 /// 当token失效或用户主动登出时发送 static let userDidLogout = Notification.Name("UserDidLogoutNotification") }