feat: 登录成功

This commit is contained in:
jinyaqiu 2025-08-27 14:34:23 +08:00
parent 3165c1e7ea
commit f8a6815d98
4 changed files with 319 additions and 105 deletions

View File

@ -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)
}
}
//

View File

@ -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")
/// tokentoken
/// 300token5
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: tokentruefalse
///
/// tokentokentoken
///
/// - Note: tokentoken
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)")
// 200token
isValid = true
print(" TokenManager: 状态码200假设token有效")
self.logger.debug(" 状态码200假设token有效")
}
} else {
// 200token
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? {
// JWTtoken
// 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<TokenResponse, NetworkError>) 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...")
// Keychaintoken
KeychainHelper.clearTokens()
// token
// UserDefaultstoken
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")
}

View File

@ -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..<length).map{ _ in letters.randomElement()! })
}
// MARK: - Coordinator
private class Coordinator: NSObject, ASAuthorizationControllerDelegate {
let onCompletion: (Result<ASAuthorization, Error>) -> Void
private let logger: Logger
init(onCompletion: @escaping (Result<ASAuthorization, Error>) -> Void) {
init(onCompletion: @escaping (Result<ASAuthorization, Error>) -> 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
}
}
}