diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index 64481b2..ce3ab0a 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/Utils/APIConfig.swift b/wake/Utils/APIConfig.swift index d92ff1d..a65ae2a 100644 --- a/wake/Utils/APIConfig.swift +++ b/wake/Utils/APIConfig.swift @@ -3,11 +3,19 @@ import Foundation /// API 配置信息 public enum APIConfig { /// API 基础 URL - public static let baseURL = "https://api.memorywake.com/api/v1" - - /// 认证 token - 生产环境中应该存储在 Keychain 中 - public static let authToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNjM0ODY2MTE1MDc2NDY0NjQsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTg4OCIsImV4cCI6MTc1NjE5NjgxNX0.hRC_So6LHuR6Gx-bDyO8aliVOd-Xumul8M7cydi2pTxHPweBx4421AfZ5BjGoEEwRZPIXJ5z7a1aDB7qvjpLCA" + public static let baseURL = "https://api-dev.memorywake.com:31274/api/v1" + /// 认证 token - 从 Keychain 中获取 + public static var authToken: String { + let token = KeychainHelper.getAccessToken() ?? "" + if !token.isEmpty { + print("🔑 [APIConfig] 当前访问令牌: \(token.prefix(10))...") // 只打印前10个字符,避免敏感信息完全暴露 + } else { + print("⚠️ [APIConfig] 未找到访问令牌") + } + return token + } + /// 认证请求头 public static var authHeaders: [String: String] { return [ @@ -16,4 +24,4 @@ public enum APIConfig { "Accept": "application/json" ] } -} +} \ No newline at end of file diff --git a/wake/Utils/KeychainHelper.swift b/wake/Utils/KeychainHelper.swift new file mode 100644 index 0000000..b2c4127 --- /dev/null +++ b/wake/Utils/KeychainHelper.swift @@ -0,0 +1,83 @@ +import Foundation +import Security + +/// 用于安全存储和检索敏感信息(如令牌)的 Keychain 帮助类 +public class KeychainHelper { + // Keychain 键名 + private enum KeychainKey: String { + case accessToken = "com.memorywake.accessToken" + case refreshToken = "com.memorywake.refreshToken" + } + + /// 保存访问令牌到 Keychain + public static func saveAccessToken(_ token: String) -> Bool { + return save(token, for: .accessToken) + } + + /// 从 Keychain 获取访问令牌 + public static func getAccessToken() -> String? { + return get(for: .accessToken) + } + + /// 保存刷新令牌到 Keychain + public static func saveRefreshToken(_ token: String) -> Bool { + return save(token, for: .refreshToken) + } + + /// 从 Keychain 获取刷新令牌 + public static func getRefreshToken() -> String? { + return get(for: .refreshToken) + } + + /// 删除所有存储的令牌 + public static func clearTokens() { + delete(for: .accessToken) + delete(for: .refreshToken) + } + + // MARK: - 私有方法 + + private static func save(_ string: String, for key: KeychainKey) -> Bool { + guard let data = string.data(using: .utf8) else { return false } + + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key.rawValue, + kSecValueData as String: data + ] + + // 先删除已存在的项目 + SecItemDelete(query as CFDictionary) + + // 添加新项目 + let status = SecItemAdd(query as CFDictionary, nil) + return status == errSecSuccess + } + + private static func get(for key: KeychainKey) -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key.rawValue, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + + var dataTypeRef: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) + + guard status == errSecSuccess, let data = dataTypeRef as? Data else { + return nil + } + + return String(data: data, encoding: .utf8) + } + + private static func delete(for key: KeychainKey) { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key.rawValue + ] + + SecItemDelete(query as CFDictionary) + } +} diff --git a/wake/View/Login/Login.swift b/wake/View/Login/Login.swift index 7b34b71..5a5e02d 100644 --- a/wake/View/Login/Login.swift +++ b/wake/View/Login/Login.swift @@ -12,7 +12,6 @@ struct LoginView: View { @State private var errorMessage = "" @State private var currentNonce: String? @State private var isLoggedIn = false - // MARK: - Body var body: some View { @@ -196,9 +195,12 @@ struct LoginView: View { print("🔵 [Apple ID] 准备调用后端认证...") authenticateWithBackend( - userId: userId, - email: email, - name: fullName, + userId: appleIDCredential.user, + email: appleIDCredential.email ?? "", + name: [appleIDCredential.fullName?.givenName, + appleIDCredential.fullName?.familyName] + .compactMap { $0 } + .joined(separator: " "), identityToken: identityToken, authCode: authCode ) @@ -216,37 +218,160 @@ struct LoginView: View { isLoading = true print("🔵 [Backend] 开始后端认证...") - let url = "https://your-api-endpoint.com/api/auth/apple" + let endpoint = "\(APIConfig.baseURL)/iam/login/oauth" + guard let url = URL(string: endpoint) else { + print("❌ [Backend] 无效的URL: \(endpoint)") + self.handleAuthenticationError(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"])) + return + } + var parameters: [String: Any] = [ - "appleUserId": userId, + "provider": "Apple", + "token": identityToken, + "userId": userId, "email": email, "name": name, - "identityToken": identityToken ] if let authCode = authCode { - parameters["authorizationCode"] = authCode + parameters["authorization_code"] = authCode } + print("📤 [Backend] 请求URL: \(endpoint)") print("📤 [Backend] 请求参数: \(parameters)") - AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default) - .validate() - .responseJSON { response in + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + do { + request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) + } catch { + print("❌ [Backend] 参数序列化失败: \(error.localizedDescription)") + self.handleAuthenticationError(error) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + DispatchQueue.main.async { self.isLoading = false - switch response.result { - case .success(let value): - print("✅ [Backend] 认证成功: \(value)") - self.handleSuccessfulAuthentication() - case .failure(let error): - print("❌ [Backend] 认证失败: \(error.localizedDescription)") - if let data = response.data, let json = String(data: data, encoding: .utf8) { - print("❌ [Backend] 错误详情: \(json)") - } + // 1. 处理网络错误 + if let error = error { + print("❌ [Backend] 请求失败: \(error.localizedDescription)") self.handleAuthenticationError(error) + return + } + + // 2. 检查响应 + guard let httpResponse = response as? HTTPURLResponse else { + let error = NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"]) + print("❌ [Backend] \(error.localizedDescription)") + self.handleAuthenticationError(error) + return + } + + // 3. 打印响应状态码和头部信息 + let statusCode = httpResponse.statusCode + print(""" + 📥 [Backend] 响应信息: + - 状态码: \(statusCode) + - URL: \(httpResponse.url?.absoluteString ?? "N/A") + - Headers: \(httpResponse.allHeaderFields) + """) + + // 4. 处理响应数据 + if let data = data { + if let jsonString = String(data: data, encoding: .utf8) { + print("📦 [Backend] 响应内容: \(jsonString)") + } + + // 5. 解析响应数据 + do { + // 首先解析顶层 JSON + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + print("✅ [Backend] 响应解析成功") + print("📦 [Backend] 响应内容: \(json)") + + // 检查状态码 + if let code = json["code"] as? Int, code != 0 { + let errorMsg = json["message"] as? String ?? "未知错误" + print("❌ [Backend] 请求失败: \(errorMsg)") + self.showError(message: errorMsg) + return + } + + // 获取 data 字典 + guard let responseData = json["data"] as? [String: Any] else { + print("⚠️ [Backend] 未找到 data 字段") + self.showError(message: "服务器返回数据格式错误") + return + } + + // 获取 user_login_info 字典 + if let userLoginInfo = responseData["user_login_info"] as? [String: Any] { + print("👤 [Backend] 用户登录信息: \(userLoginInfo)") + + // 保存令牌到 Keychain + if let accessToken = userLoginInfo["access_token"] as? String { + _ = KeychainHelper.saveAccessToken(accessToken) + print("🔑 [Keychain] 访问令牌已保存") + + // 保存用户信息到 UserDefaults + var userInfo: [String: Any] = [ + "user_id": userLoginInfo["user_id"] as? Int64 ?? 0, + "account": userLoginInfo["account"] as? String ?? "", + "nickname": userLoginInfo["nickname"] as? String ?? "", + "avatar": userLoginInfo["avatar_file_url"] as? String ?? "" + ] + UserDefaults.standard.set(userInfo, forKey: "currentUserInfo") + print("👤 [UserDefaults] 用户信息已保存") + } + + if let refreshToken = userLoginInfo["refresh_token"] as? String { + _ = KeychainHelper.saveRefreshToken(refreshToken) + print("🔄 [Keychain] 刷新令牌已保存") + } + + // 处理认证成功 + DispatchQueue.main.async { + self.handleSuccessfulAuthentication() + } + return + } else { + print("⚠️ [Backend] 未找到 user_login_info 字段") + self.showError(message: "登录信息不完整") + } + } + } catch { + print("⚠️ [Backend] 响应解析失败: \(error.localizedDescription)") + self.showError(message: "数据解析失败") + } + } + + // 6. 如果上面的 return 没有执行,说明认证失败 + if statusCode < 200 || statusCode >= 300 { + let errorMessage: String + switch statusCode { + case 400: + errorMessage = "请求参数错误" + case 401: + errorMessage = "认证失败,请重新登录" + case 403: + errorMessage = "权限不足" + case 404: + errorMessage = "请求的接口不存在" + case 500...599: + errorMessage = "服务器内部错误,请稍后重试" + default: + errorMessage = "未知错误 (状态码: \(statusCode))" + } + self.showError(message: errorMessage) } } + } + + task.resume() } // MARK: - Helpers @@ -264,7 +389,7 @@ struct LoginView: View { showError(message: "登录失败: \(error.localizedDescription)") } - private func handleAuthenticationError(_ error: AFError) { + private func handleAuthenticationError(_ error: Error) { let errorMessage = error.localizedDescription print("❌ [Auth] 认证错误: \(errorMessage)") DispatchQueue.main.async {