wake-ios/wake/Utils/TokenManager.swift
2025-09-01 19:42:32 +08:00

358 lines
13 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import OSLog
/// Token
///
class TokenManager {
///
static let shared = TokenManager()
private let logger = Logger(subsystem: "com.yourapp.tokenmanager", category: "TokenManager")
/// tokentoken
private let tokenValidityThreshold: TimeInterval = 300
///
private init() {}
// MARK: - Token
/// 访
var hasToken: Bool {
let hasToken = KeychainHelper.getAccessToken()?.isEmpty == false
logger.debug("检查token存在状态: \(hasToken ? "存在" : "不存在")")
return hasToken
}
// MARK: - Token
/// token
func validateAndRefreshTokenIfNeeded(completion: @escaping (Bool, Error?) -> Void) {
logger.debug("开始验证token状态...")
// 1. token
guard let token = KeychainHelper.getAccessToken(), !token.isEmpty else {
let error = NSError(
domain: "TokenManager",
code: 401,
userInfo: [NSLocalizedDescriptionKey: "未找到访问令牌"]
)
logger.error("❌ Token验证失败: 未找到访问令牌")
completion(false, error)
return
}
// 2. token
if isTokenValid(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
public func isTokenValid(_ token: String) -> Bool {
logger.debug("开始验证token有效性...")
// 1. token
guard !token.isEmpty else {
logger.error("❌ Token为空")
return false
}
// 2. token
if let expiryDate = getTokenExpiryDate(token) {
logger.debug("Token过期时间: \(expiryDate)")
if Date() > expiryDate {
logger.error("❌ Token已过期")
return false
}
// 5
let timeRemaining = expiryDate.timeIntervalSinceNow
logger.debug("Token剩余有效时间: \(Int(timeRemaining))")
if timeRemaining < tokenValidityThreshold {
logger.debug("⚠️ Token即将过期需要刷新")
return false
}
}
// 3. token
let semaphore = DispatchSemaphore(value: 0)
var isValid = false
var requestCompleted = false
let validationRequest = createValidationRequest(token: token)
logger.debug("发送Token验证请求: \(validationRequest.url?.absoluteString ?? "未知URL")")
logger.debug("请求头: \(validationRequest.allHTTPHeaderFields ?? [:])")
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 {
self.logger.error("❌ Token验证请求错误: \(error.localizedDescription) (耗时: \(responseTime))")
return
}
//
guard let httpResponse = response as? HTTPURLResponse else {
self.logger.error("❌ 无效的服务器响应 (耗时: \(responseTime))")
return
}
let statusCode = httpResponse.statusCode
self.logger.debug("收到Token验证响应 - 状态码: \(statusCode) (耗时: \(responseTime))")
//
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
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 {
self.logger.error("❌ 解析响应数据失败: \(error.localizedDescription)")
// 200token
isValid = true
self.logger.debug(" 状态码200假设token有效")
}
} else {
self.logger.debug(" 没有返回数据但状态码为200假设token有效")
isValid = true
}
}
task.resume()
// 15
let timeoutResult = semaphore.wait(timeout: .now() + 15)
//
if !requestCompleted && timeoutResult == .timedOut {
logger.error("⚠️ Token验证请求超时")
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? {
logger.debug("开始解析token过期时间...")
let parts = token.components(separatedBy: ".")
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 {
let expiryDate = Date(timeIntervalSince1970: exp)
logger.debug("✅ 成功解析token过期时间: \(expiryDate)")
return expiryDate
}
} catch {
logger.error("❌ 解析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 = base64 + padding
}
return Data(base64Encoded: base64)
}
/// token
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):
self.logger.debug("✅ Token刷新成功 (耗时: \(responseTime))")
self.logger.debug("新的access_token: \(tokenResponse.accessToken.prefix(10))...")
if let newRefreshToken = tokenResponse.refreshToken {
self.logger.debug("新的refresh_token: \(newRefreshToken.prefix(10))...")
KeychainHelper.saveRefreshToken(newRefreshToken)
}
if let expiresIn = tokenResponse.expiresIn {
self.logger.debug("Token有效期: \(expiresIn)")
}
// 访
KeychainHelper.saveAccessToken(tokenResponse.accessToken)
completion(true, nil)
case .failure(let error):
self.logger.error("❌ Token刷新失败 (耗时: \(responseTime)): \(error.localizedDescription)")
// token
self.logger.debug("清除所有token...")
KeychainHelper.clearTokens()
completion(false, error)
}
}
}
/// token
func clearTokens() {
logger.debug("开始清除所有token...")
// Keychaintoken
KeychainHelper.clearTokens()
// UserDefaultstoken
UserDefaults.standard.removeObject(forKey: "tokenExpiryDate")
UserDefaults.standard.synchronize()
logger.debug("✅ 所有token已清除")
//
NotificationCenter.default.post(name: .userDidLogout, object: nil)
logger.debug("已发送登出通知")
}
}
// MARK: -
private struct TokenResponse: Codable {
let accessToken: String
let refreshToken: String?
let expiresIn: TimeInterval?
let tokenType: String?
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case refreshToken = "refresh_token"
case expiresIn = "expires_in"
case tokenType = "token_type"
}
}
private struct IdentityCheckResponse: Codable {
let isValid: Bool
let userId: String?
let expiresAt: Date?
enum CodingKeys: String, CodingKey {
case isValid = "is_valid"
case userId = "user_id"
case expiresAt = "expires_at"
}
}
// MARK: -
extension Notification.Name {
static let userDidLogout = Notification.Name("UserDidLogoutNotification")
}