358 lines
13 KiB
Swift
358 lines
13 KiB
Swift
import Foundation
|
||
import OSLog
|
||
|
||
/// Token管理器
|
||
/// 负责管理应用的认证令牌,包括验证、刷新和过期处理
|
||
class TokenManager {
|
||
/// 单例实例
|
||
static let shared = TokenManager()
|
||
|
||
private let logger = Logger(subsystem: "com.yourapp.tokenmanager", category: "TokenManager")
|
||
|
||
/// token有效期阈值(秒),在token即将过期前进行刷新
|
||
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)")
|
||
// 如果解析失败但状态码是200,我们假设token是有效的
|
||
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...")
|
||
|
||
// 清除Keychain中的token
|
||
KeychainHelper.clearTokens()
|
||
|
||
// 清除UserDefaults中的token相关信息
|
||
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")
|
||
}
|