From f8a6815d9874fc9486a2973991e9efbc9c669e71 Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Wed, 27 Aug 2025 14:34:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=99=BB=E5=BD=95=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/NetworkService.swift | 8 +- wake/Utils/TokenManager.swift | 192 +++++++++------- wake/View/Components/AppleSignInButton.swift | 222 +++++++++++++++++-- wake/View/Login/Login.swift | 2 +- 4 files changed, 319 insertions(+), 105 deletions(-) diff --git a/wake/Utils/NetworkService.swift b/wake/Utils/NetworkService.swift index a26f7c3..118c605 100644 --- a/wake/Utils/NetworkService.swift +++ b/wake/Utils/NetworkService.swift @@ -169,9 +169,11 @@ class NetworkService { request.setValue(value, forHTTPHeaderField: key) } - // 添加认证头 - APIConfig.authHeaders.forEach { key, value in - request.setValue(value, forHTTPHeaderField: key) + // 添加认证头 - 排除登录接口 + if !path.contains("/iam/login/") { + APIConfig.authHeaders.forEach { key, value in + request.setValue(value, forHTTPHeaderField: key) + } } // 添加自定义头(如果提供) diff --git a/wake/Utils/TokenManager.swift b/wake/Utils/TokenManager.swift index ad6c2d9..7305bc7 100644 --- a/wake/Utils/TokenManager.swift +++ b/wake/Utils/TokenManager.swift @@ -1,4 +1,5 @@ import Foundation +import OSLog /// Token管理器 /// 负责管理应用的认证令牌,包括验证、刷新和过期处理 @@ -6,8 +7,9 @@ class TokenManager { /// 单例实例 static let shared = TokenManager() + private let logger = Logger(subsystem: "com.yourapp.tokenmanager", category: "TokenManager") + /// token有效期阈值(秒),在token即将过期前进行刷新 - /// 例如:设置为300表示在token过期前5分钟开始刷新 private let tokenValidityThreshold: TimeInterval = 300 /// 私有化初始化方法,确保单例模式 @@ -17,142 +19,162 @@ class TokenManager { /// 检查是否存在有效的访问令牌 var hasToken: Bool { - return KeychainHelper.getAccessToken()?.isEmpty == false + let hasToken = KeychainHelper.getAccessToken()?.isEmpty == false + logger.debug("检查token存在状态: \(hasToken ? "存在" : "不存在")") + return hasToken } // MARK: - Token 验证 /// 验证并刷新token(如果需要) - /// - 检查token是否存在 - /// - 检查token是否有效 - /// - 在token即将过期时自动刷新 - /// - Parameter completion: 完成回调,返回验证/刷新结果 - /// - isValid: token是否有效 - /// - error: 错误信息(如果有) func validateAndRefreshTokenIfNeeded(completion: @escaping (Bool, Error?) -> Void) { + logger.debug("开始验证token状态...") + // 1. 检查token是否存在 guard let token = KeychainHelper.getAccessToken(), !token.isEmpty else { - // token不存在,返回未授权错误 let error = NSError( domain: "TokenManager", code: 401, userInfo: [NSLocalizedDescriptionKey: "未找到访问令牌"] ) + logger.error("❌ Token验证失败: 未找到访问令牌") completion(false, error) return } // 2. 检查token是否有效 if isTokenValid(token) { - // token有效,直接返回成功 + logger.debug("✅ Token验证通过,无需刷新") completion(true, nil) return } + logger.debug("🔄 Token需要刷新,开始刷新流程...") + // 3. token无效或即将过期,尝试刷新 refreshToken { [weak self] success, error in if success { - // 刷新成功,返回成功 + self?.logger.debug("✅ Token刷新成功") completion(true, nil) } else { - // 刷新失败,返回错误信息 let finalError = error ?? NSError( domain: "TokenManager", code: 401, userInfo: [NSLocalizedDescriptionKey: "Token刷新失败"] ) + self?.logger.error("❌ Token刷新失败: \(finalError.localizedDescription)") 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...") + logger.debug("开始验证token有效性...") // 1. 基础验证:检查token是否为空 guard !token.isEmpty else { - print("❌ TokenManager: Token为空") + logger.error("❌ Token为空") return false } - // 2. 检查token是否过期(如果可能) + // 2. 检查token是否过期 if let expiryDate = getTokenExpiryDate(token) { - print("⏰ TokenManager: Token过期时间: \(expiryDate)") + logger.debug("Token过期时间: \(expiryDate)") if Date() > expiryDate { - print("❌ TokenManager: Token已过期") + logger.error("❌ Token已过期") + return false + } + + // 检查是否需要刷新(在过期前5分钟) + let timeRemaining = expiryDate.timeIntervalSinceNow + logger.debug("Token剩余有效时间: \(Int(timeRemaining))秒") + + if timeRemaining < tokenValidityThreshold { + logger.debug("⚠️ Token即将过期,需要刷新") return false } } - // 3. 创建信号量用于同步网络请求 + // 3. 验证token有效性 let semaphore = DispatchSemaphore(value: 0) var isValid = false var requestCompleted = false - print("🌐 TokenManager: 发送验证请求到服务器...") + let validationRequest = createValidationRequest(token: token) + logger.debug("发送Token验证请求: \(validationRequest.url?.absoluteString ?? "未知URL")") + logger.debug("请求头: \(validationRequest.allHTTPHeaderFields ?? [:])") - // 4. 发送验证请求 - let task = URLSession.shared.dataTask(with: createValidationRequest(token: token)) { data, response, error in + let startTime = Date() + + let task = URLSession.shared.dataTask(with: validationRequest) { data, response, error in defer { requestCompleted = true semaphore.signal() } + let responseTime = String(format: "%.2f秒", Date().timeIntervalSince(startTime)) + // 检查网络错误 if let error = error { - print("❌ TokenManager: 验证请求错误: \(error.localizedDescription)") + self.logger.error("❌ Token验证请求错误: \(error.localizedDescription) (耗时: \(responseTime))") return } // 检查响应状态码 guard let httpResponse = response as? HTTPURLResponse else { - print("❌ TokenManager: 无效的服务器响应") + self.logger.error("❌ 无效的服务器响应 (耗时: \(responseTime))") return } - print("📡 TokenManager: 服务器响应状态码: \(httpResponse.statusCode)") + let statusCode = httpResponse.statusCode + self.logger.debug("收到Token验证响应 - 状态码: \(statusCode) (耗时: \(responseTime))") // 检查状态码 - guard (200...299).contains(httpResponse.statusCode) else { - print("❌ TokenManager: 服务器返回错误状态码: \(httpResponse.statusCode)") + guard (200...299).contains(statusCode) else { + self.logger.error("❌ 服务器返回错误状态码: \(statusCode)") return } - // 检查是否有数据 + // 检查响应数据 if let data = data, !data.isEmpty { do { - // 尝试解析响应数据 + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + self.logger.debug("Token验证响应数据: \(json)") + } + let response = try JSONDecoder().decode(IdentityCheckResponse.self, from: data) isValid = response.isValid - print("✅ TokenManager: Token验证\(isValid ? "成功" : "失败")") + self.logger.debug("✅ Token验证\(isValid ? "成功" : "失败")") + + if let userId = response.userId { + self.logger.debug("用户ID: \(userId)") + } + + if let expiresAt = response.expiresAt { + self.logger.debug("Token过期时间: \(expiresAt)") + } + } catch { - print("❌ TokenManager: 解析响应数据失败: \(error.localizedDescription)") + self.logger.error("❌ 解析响应数据失败: \(error.localizedDescription)") // 如果解析失败但状态码是200,我们假设token是有效的 isValid = true - print("ℹ️ TokenManager: 状态码200,假设token有效") + self.logger.debug("ℹ️ 状态码200,假设token有效") } } else { - // 如果没有返回数据但状态码是200,我们假设token是有效的 - print("ℹ️ TokenManager: 没有返回数据,但状态码为200,假设token有效") + self.logger.debug("ℹ️ 没有返回数据,但状态码为200,假设token有效") isValid = true } } task.resume() - // 5. 设置超时时间(10秒) + // 设置超时时间(15秒) let timeoutResult = semaphore.wait(timeout: .now() + 15) // 检查是否超时 if !requestCompleted && timeoutResult == .timedOut { - print("⚠️ TokenManager: 验证请求超时") + logger.error("⚠️ Token验证请求超时") task.cancel() return false } @@ -170,22 +192,30 @@ class TokenManager { return request } - /// 从token中提取过期时间(示例实现) + /// 从token中提取过期时间 private func getTokenExpiryDate(_ token: String) -> Date? { - // 这里需要根据实际的JWT或其他token格式来解析过期时间 - // 以下是JWT token的示例解析 + logger.debug("开始解析token过期时间...") + let parts = token.components(separatedBy: ".") - guard parts.count > 1, let payloadData = base64UrlDecode(parts[1]) else { + guard parts.count > 1 else { + logger.error("❌ 无效的token格式") + return nil + } + + guard let payloadData = base64UrlDecode(parts[1]) else { + logger.error("❌ 无法解码token payload") return nil } do { if let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any], let exp = payload["exp"] as? TimeInterval { - return Date(timeIntervalSince1970: exp) + let expiryDate = Date(timeIntervalSince1970: exp) + logger.debug("✅ 成功解析token过期时间: \(expiryDate)") + return expiryDate } } catch { - print("❌ TokenManager: 解析token过期时间失败: \(error.localizedDescription)") + logger.error("❌ 解析token过期时间失败: \(error.localizedDescription)") } return nil @@ -209,49 +239,65 @@ class TokenManager { } /// 刷新token - /// - Parameter completion: 刷新完成回调 - /// - success: 是否刷新成功 - /// - error: 错误信息(如果有) func refreshToken(completion: @escaping (Bool, Error?) -> Void) { + logger.debug("开始刷新token...") + // 获取刷新令牌 guard let refreshToken = KeychainHelper.getRefreshToken(), !refreshToken.isEmpty else { - // 没有可用的刷新令牌 let error = NSError( domain: "TokenManager", code: 401, userInfo: [NSLocalizedDescriptionKey: "未找到刷新令牌"] ) + logger.error("❌ 刷新token失败: 未找到刷新令牌") completion(false, error) return } + logger.debug("找到刷新令牌,准备请求...") + // 准备刷新请求参数 let parameters: [String: Any] = [ "refresh_token": refreshToken, "grant_type": "refresh_token" ] + let url = APIConfig.baseURL + "/v1/iam/access-token-refresh" + logger.debug("发送刷新token请求到: \(url)") + logger.debug("请求参数: \(parameters)") + + let startTime = Date() + // 发送刷新请求 NetworkService.shared.post(path: "/v1/iam/access-token-refresh", parameters: parameters) { (result: Result) in + let responseTime = String(format: "%.2f秒", Date().timeIntervalSince(startTime)) + switch result { case .success(let tokenResponse): - // 1. 保存新的访问令牌 - KeychainHelper.saveAccessToken(tokenResponse.accessToken) + self.logger.debug("✅ Token刷新成功 (耗时: \(responseTime))") + self.logger.debug("新的access_token: \(tokenResponse.accessToken.prefix(10))...") - // 2. 如果返回了新的刷新令牌,也保存起来 if let newRefreshToken = tokenResponse.refreshToken { + self.logger.debug("新的refresh_token: \(newRefreshToken.prefix(10))...") KeychainHelper.saveRefreshToken(newRefreshToken) } - print("✅ Token刷新成功") + if let expiresIn = tokenResponse.expiresIn { + self.logger.debug("Token有效期: \(expiresIn)秒") + } + + // 保存新的访问令牌 + KeychainHelper.saveAccessToken(tokenResponse.accessToken) + completion(true, nil) case .failure(let error): - print("❌ Token刷新失败: \(error.localizedDescription)") + self.logger.error("❌ Token刷新失败 (耗时: \(responseTime)): \(error.localizedDescription)") // 刷新失败,清除本地token,需要用户重新登录 + self.logger.debug("清除所有token...") KeychainHelper.clearTokens() completion(false, error) @@ -261,30 +307,30 @@ class TokenManager { /// 清除所有存储的 token func clearTokens() { - print("🗑️ TokenManager: 清除所有 token") + logger.debug("开始清除所有token...") + + // 清除Keychain中的token KeychainHelper.clearTokens() - // 清除其他与 token 相关的存储 + + // 清除UserDefaults中的token相关信息 UserDefaults.standard.removeObject(forKey: "tokenExpiryDate") UserDefaults.standard.synchronize() + + logger.debug("✅ 所有token已清除") + + // 发送登出通知 + NotificationCenter.default.post(name: .userDidLogout, object: nil) + logger.debug("已发送登出通知") } } -// MARK: - Token响应模型 -/// 用于解析token刷新接口的响应数据 +// MARK: - 响应模型 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" @@ -293,16 +339,9 @@ private struct TokenResponse: Codable { } } -// MARK: - 身份验证响应模型 -/// 用于解析身份验证接口的响应数据 private struct IdentityCheckResponse: Codable { - /// 是否有效 let isValid: Bool - - /// 用户ID(可选) let userId: String? - - /// 过期时间(可选) let expiresAt: Date? enum CodingKeys: String, CodingKey { @@ -313,9 +352,6 @@ private struct IdentityCheckResponse: Codable { } // MARK: - 通知名称 -/// 定义应用中使用的通知名称 extension Notification.Name { - /// 用户登出通知 - /// 当token失效或用户主动登出时发送 static let userDidLogout = Notification.Name("UserDidLogoutNotification") } diff --git a/wake/View/Components/AppleSignInButton.swift b/wake/View/Components/AppleSignInButton.swift index cd61af0..3a0f469 100644 --- a/wake/View/Components/AppleSignInButton.swift +++ b/wake/View/Components/AppleSignInButton.swift @@ -1,6 +1,7 @@ import SwiftUI import AuthenticationServices import CryptoKit +import os.log /// 自定义的 Apple 登录按钮组件 struct AppleSignInButton: View { @@ -15,6 +16,12 @@ struct AppleSignInButton: View { /// 按钮文字 let buttonText: String + // 创建日志记录器 + private let logger = Logger(subsystem: "com.yourapp", category: "AppleSignInButton") + + // 添加一个强引用到coordinator + @State private var coordinator: Coordinator? + // MARK: - 初始化方法 init(buttonText: String = "Continue with Apple", @@ -23,48 +30,117 @@ struct AppleSignInButton: View { self.buttonText = buttonText self.onRequest = onRequest self.onCompletion = onCompletion + logger.debug("AppleSignInButton 初始化,按钮文字: \(buttonText)") } // MARK: - 视图主体 var body: some View { Button(action: handleSignIn) { - HStack(alignment: .center, spacing: 8) { - Image(systemName: "applelogo") - .font(.system(size: 20, weight: .regular)) - Text(buttonText) - .font(.system(size: 18, weight: .regular)) - } - .frame(maxWidth: .infinity) - .frame(height: 60) - .background(Color.white) - .foregroundColor(.black) - .cornerRadius(30) - .overlay( - RoundedRectangle(cornerRadius: 30) - .stroke(Color.black, lineWidth: 1) // 使用黑色边框 - ) - } + HStack(alignment: .center, spacing: 8) { + Image(systemName: "applelogo") + .font(.system(size: 20, weight: .regular)) + Text(buttonText) + .font(.system(size: 18, weight: .regular)) + } + .frame(maxWidth: .infinity) + .frame(height: 60) + .background(Color.white) + .foregroundColor(.black) + .cornerRadius(30) + .overlay( + RoundedRectangle(cornerRadius: 30) + .stroke(Color.black, lineWidth: 1) + ) + } } // MARK: - 私有方法 private func handleSignIn() { + logger.debug("🍎 用户点击了Apple登录按钮") + let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName, .email] // 创建 nonce 用于安全验证 - let nonce = String.randomURLSafeString(length: 32) + let nonce = randomNonceString(length: 32) + logger.debug("🔑 生成Nonce: \(nonce)") + request.nonce = sha256(nonce) + logger.debug("🔐 Nonce的SHA256: \(request.nonce ?? "nil")") // 调用请求回调 + logger.debug("📞 调用onRequest回调") onRequest(request) // 创建并显示授权控制器 + logger.debug("🔄 创建ASAuthorizationController") let controller = ASAuthorizationController(authorizationRequests: [request]) - controller.delegate = Coordinator(onCompletion: onCompletion) - controller.performRequests() + + // 创建presentation context provider + let presentationContextProvider = PresentationContextProvider() + + // 创建coordinator并保持强引用 + let newCoordinator = Coordinator( + onCompletion: { [logger] result in + // 处理完成后释放coordinator + DispatchQueue.main.async { + self.coordinator = nil + } + + switch result { + case .success(let auth): + logger.debug("✅ 授权成功 - 开始处理授权响应") + + if let appleIDCredential = auth.credential as? ASAuthorizationAppleIDCredential { + let userIdentifier = appleIDCredential.user + logger.debug("👤 用户标识符: \(userIdentifier)") + + // 保存用户ID用于后续验证 + UserDefaults.standard.set(userIdentifier, forKey: "appleAuthorizedUserIdKey") + } + case .failure(let error): + let nsError = error as NSError + logger.error("❌ 授权失败: \(nsError.localizedDescription)") + } + + // 调用完成回调 + self.onCompletion(result) + }, + logger: logger + ) + + // 保存coordinator的强引用 + self.coordinator = newCoordinator + + // 设置代理和presentation context provider + controller.delegate = newCoordinator + controller.presentationContextProvider = presentationContextProvider + + // 执行请求 + logger.debug("🚀 开始执行授权请求...") + DispatchQueue.main.async { + controller.performRequests() + } + } + + private func logAppleIDError(_ error: ASAuthorizationError, logger: Logger) { + switch error.code { + case .canceled: + logger.error("❌ 用户取消了授权") + case .failed: + logger.error("❌ 授权请求失败") + case .invalidResponse: + logger.error("❌ 无效的授权响应") + case .notHandled: + logger.error("❌ 授权请求未被处理") + case .unknown: + logger.error("❌ 未知的授权错误") + @unknown default: + logger.error("❌ 未处理的授权错误") + } } private func sha256(_ input: String) -> String { @@ -73,23 +149,123 @@ struct AppleSignInButton: View { return hashedData.compactMap { String(format: "%02x", $0) }.joined() } - // MARK: - 协调器 + // 使用项目中的现有方法生成随机字符串 + private func randomNonceString(length: Int) -> String { + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return String((0..) -> Void + private let logger: Logger - init(onCompletion: @escaping (Result) -> Void) { + init(onCompletion: @escaping (Result) -> Void, logger: Logger) { self.onCompletion = onCompletion + self.logger = logger + super.init() + logger.debug("Coordinator 初始化") } // 授权成功回调 - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + func authorizationController(controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization) { + logger.debug("✅ 授权成功 - 开始处理授权响应") + + if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { + // 创建用户标识符 + let userIdentifier = appleIDCredential.user + logger.debug("👤 用户标识符: \(userIdentifier)") + + // 检查是否是新用户 + if let email = appleIDCredential.email, let fullName = appleIDCredential.fullName { + logger.debug("👋 检测到新用户") + logger.debug("📧 新用户邮箱: \(email)") + logger.debug("👤 新用户全名: \(fullName.givenName ?? "") \(fullName.familyName ?? "")") + } else { + logger.debug("👋 现有用户登录") + } + + // 保存用户标识符到UserDefaults + UserDefaults.standard.set(userIdentifier, forKey: "appleAuthorizedUserIdKey") + + // 获取授权码 + if let authCodeData = appleIDCredential.authorizationCode, + let _ = String(data: authCodeData, encoding: .utf8) { + logger.debug("🔑 成功获取授权码") + } else { + logger.warning("⚠️ 未获取到授权码") + } + + // 获取身份令牌 + if let identityTokenData = appleIDCredential.identityToken, + let _ = String(data: identityTokenData, encoding: .utf8) { + logger.debug("🔐 成功获取身份令牌") + } else { + logger.error("❌ 获取身份令牌失败") + } + } else { + logger.error("❌ 无法获取有效的 Apple ID 凭证") + } + + // 调用完成处理程序 + logger.debug("🔄 调用完成处理程序") onCompletion(.success(authorization)) } // 授权失败回调 - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + func authorizationController(controller: ASAuthorizationController, + didCompleteWithError error: Error) { + let nsError = error as NSError + logger.error("❌ 授权失败: \(nsError.localizedDescription)") + logger.error("错误域: \(nsError.domain), 错误码: \(nsError.code)") + + if let appleIDError = error as? ASAuthorizationError { + switch appleIDError.code { + case .canceled: + logger.error("❌ 用户取消了授权") + case .failed: + logger.error("❌ 授权请求失败") + case .invalidResponse: + logger.error("❌ 无效的授权响应") + case .notHandled: + logger.error("❌ 授权请求未被处理") + case .unknown: + logger.error("❌ 未知的授权错误") + @unknown default: + logger.error("❌ 未处理的授权错误") + } + + // 记录更多错误详情 + if let errorString = (appleIDError as NSError).userInfo[NSDebugDescriptionErrorKey] as? String { + logger.error("🔍 错误详情: \(errorString)") + } + } + + // 调用完成处理程序 + logger.debug("🔄 调用完成处理程序(错误)") onCompletion(.failure(error)) } } + + // MARK: - Presentation Context Provider + + private class PresentationContextProvider: NSObject, ASAuthorizationControllerPresentationContextProviding { + private let logger = Logger(subsystem: "com.yourapp.applesignin", category: "PresentationContextProvider") + + override init() { + super.init() + logger.debug("PresentationContextProvider 初始化") + } + + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + logger.debug("获取presentation anchor") + guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { + logger.error("无法获取key window,将使用第一个window") + return UIApplication.shared.windows.first ?? UIWindow() + } + return window + } + } } \ No newline at end of file diff --git a/wake/View/Login/Login.swift b/wake/View/Login/Login.swift index 5b51152..e23c732 100644 --- a/wake/View/Login/Login.swift +++ b/wake/View/Login/Login.swift @@ -234,7 +234,7 @@ struct LoginView: View { print(" 请求参数: \(parameters.keys)") NetworkService.shared.post( - path: "/iam/login/oauth", + path: "/iam/login/oauth", parameters: parameters ) { (result: Result) -> Void in let handleResult: () -> Void = {