425 lines
15 KiB
Swift
425 lines
15 KiB
Swift
import Foundation
|
||
|
||
/// Token管理器
|
||
/// 负责管理应用的认证令牌,包括验证、刷新和过期处理
|
||
class TokenManager {
|
||
/// 单例实例
|
||
static let shared = TokenManager()
|
||
|
||
/// token有效期阈值(秒),在token即将过期前进行刷新
|
||
/// 例如:设置为300表示在token过期前5分钟开始刷新
|
||
private let tokenValidityThreshold: TimeInterval = 300
|
||
|
||
/// 私有化初始化方法,确保单例模式
|
||
private init() {}
|
||
|
||
// MARK: - Token 状态检查
|
||
|
||
/// 检查是否存在有效的访问令牌
|
||
var hasToken: Bool {
|
||
return KeychainHelper.getAccessToken()?.isEmpty == false
|
||
}
|
||
|
||
// MARK: - Token 验证
|
||
|
||
/// 验证并刷新token(如果需要)
|
||
/// - 检查token是否存在
|
||
/// - 检查token是否有效
|
||
/// - 在token即将过期时自动刷新
|
||
/// - Parameter completion: 完成回调,返回验证/刷新结果
|
||
/// - isValid: token是否有效
|
||
/// - error: 错误信息(如果有)
|
||
func validateAndRefreshTokenIfNeeded(completion: @escaping (Bool, Error?) -> Void) {
|
||
// 1. 检查token是否存在
|
||
guard let token = KeychainHelper.getAccessToken(), !token.isEmpty else {
|
||
// token不存在,返回未授权错误
|
||
let error = NSError(
|
||
domain: "TokenManager",
|
||
code: 401,
|
||
userInfo: [NSLocalizedDescriptionKey: "未找到访问令牌"]
|
||
)
|
||
completion(false, error)
|
||
return
|
||
}
|
||
|
||
// 2. 检查token是否有效
|
||
if isTokenValid(token) {
|
||
// token有效,直接返回成功
|
||
completion(true, nil)
|
||
return
|
||
}
|
||
|
||
// 3. token无效或即将过期,尝试刷新
|
||
refreshToken { [weak self] success, error in
|
||
if success {
|
||
// 刷新成功,返回成功
|
||
completion(true, nil)
|
||
} else {
|
||
// 刷新失败,返回错误信息
|
||
let finalError = error ?? NSError(
|
||
domain: "TokenManager",
|
||
code: 401,
|
||
userInfo: [NSLocalizedDescriptionKey: "Token刷新失败"]
|
||
)
|
||
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...")
|
||
|
||
// 1. 基础验证:检查token是否为空
|
||
guard !token.isEmpty else {
|
||
print("❌ TokenManager: Token为空")
|
||
return false
|
||
}
|
||
|
||
// 2. 检查token是否过期(如果可能)
|
||
if let expiryDate = getTokenExpiryDate(token) {
|
||
print("⏰ TokenManager: Token过期时间: \(expiryDate)")
|
||
if Date() > expiryDate {
|
||
print("❌ TokenManager: Token已过期")
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 3. 创建信号量用于同步网络请求
|
||
let semaphore = DispatchSemaphore(value: 0)
|
||
var isValid = false
|
||
var requestCompleted = false
|
||
|
||
print("🌐 TokenManager: 发送验证请求到服务器...")
|
||
|
||
// 4. 发送验证请求
|
||
let task = URLSession.shared.dataTask(with: createValidationRequest(token: token)) { data, response, error in
|
||
defer {
|
||
requestCompleted = true
|
||
semaphore.signal()
|
||
}
|
||
|
||
// 检查网络错误
|
||
if let error = error {
|
||
print("❌ TokenManager: 验证请求错误: \(error.localizedDescription)")
|
||
return
|
||
}
|
||
|
||
// 检查响应状态码
|
||
guard let httpResponse = response as? HTTPURLResponse else {
|
||
print("❌ TokenManager: 无效的服务器响应")
|
||
return
|
||
}
|
||
|
||
print("📡 TokenManager: 服务器响应状态码: \(httpResponse.statusCode)")
|
||
|
||
// 检查状态码
|
||
guard (200...299).contains(httpResponse.statusCode) else {
|
||
print("❌ TokenManager: 服务器返回错误状态码: \(httpResponse.statusCode)")
|
||
return
|
||
}
|
||
|
||
// 检查是否有数据
|
||
if let data = data, !data.isEmpty {
|
||
do {
|
||
// 尝试解析响应数据
|
||
let response = try JSONDecoder().decode(IdentityCheckResponse.self, from: data)
|
||
isValid = response.isValid
|
||
print("✅ TokenManager: Token验证\(isValid ? "成功" : "失败")")
|
||
} catch {
|
||
print("❌ TokenManager: 解析响应数据失败: \(error.localizedDescription)")
|
||
// 如果解析失败但状态码是200,我们假设token是有效的
|
||
isValid = true
|
||
print("ℹ️ TokenManager: 状态码200,假设token有效")
|
||
}
|
||
} else {
|
||
// 如果没有返回数据但状态码是200,我们假设token是有效的
|
||
print("ℹ️ TokenManager: 没有返回数据,但状态码为200,假设token有效")
|
||
isValid = true
|
||
}
|
||
}
|
||
|
||
task.resume()
|
||
|
||
// 5. 设置超时时间(10秒)
|
||
let timeoutResult = semaphore.wait(timeout: .now() + 15)
|
||
|
||
// 检查是否超时
|
||
if !requestCompleted && timeoutResult == .timedOut {
|
||
print("⚠️ TokenManager: 验证请求超时")
|
||
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? {
|
||
// 这里需要根据实际的JWT或其他token格式来解析过期时间
|
||
// 以下是JWT token的示例解析
|
||
let parts = token.components(separatedBy: ".")
|
||
guard parts.count > 1, let payloadData = base64UrlDecode(parts[1]) else {
|
||
return nil
|
||
}
|
||
|
||
do {
|
||
if let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any],
|
||
let exp = payload["exp"] as? TimeInterval {
|
||
return Date(timeIntervalSince1970: exp)
|
||
}
|
||
} catch {
|
||
print("❌ TokenManager: 解析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 += padding
|
||
}
|
||
|
||
return Data(base64Encoded: base64)
|
||
}
|
||
|
||
/// 刷新token
|
||
/// - Parameter completion: 刷新完成回调
|
||
/// - success: 是否刷新成功
|
||
/// - error: 错误信息(如果有)
|
||
private func refreshToken(completion: @escaping (Bool, Error?) -> Void) {
|
||
// 获取刷新令牌
|
||
guard let refreshToken = KeychainHelper.getRefreshToken(), !refreshToken.isEmpty else {
|
||
// 没有可用的刷新令牌
|
||
let error = NSError(
|
||
domain: "TokenManager",
|
||
code: 401,
|
||
userInfo: [NSLocalizedDescriptionKey: "未找到刷新令牌"]
|
||
)
|
||
completion(false, error)
|
||
return
|
||
}
|
||
|
||
// 准备刷新请求参数
|
||
let parameters: [String: Any] = [
|
||
"refresh_token": refreshToken,
|
||
"grant_type": "refresh_token"
|
||
]
|
||
|
||
// 发送刷新请求
|
||
NetworkService.shared.postWithToken(path: "/v1/iam/access-token-refresh", parameters: parameters) {
|
||
(result: Result<TokenResponse, NetworkError>) in
|
||
|
||
switch result {
|
||
case .success(let tokenResponse):
|
||
// 1. 保存新的访问令牌
|
||
KeychainHelper.saveAccessToken(tokenResponse.accessToken)
|
||
|
||
// 2. 如果返回了新的刷新令牌,也保存起来
|
||
if let newRefreshToken = tokenResponse.refreshToken {
|
||
KeychainHelper.saveRefreshToken(newRefreshToken)
|
||
}
|
||
|
||
print("✅ Token刷新成功")
|
||
completion(true, nil)
|
||
|
||
case .failure(let error):
|
||
print("❌ Token刷新失败: \(error.localizedDescription)")
|
||
|
||
// 刷新失败,清除本地token,需要用户重新登录
|
||
KeychainHelper.clearTokens()
|
||
|
||
completion(false, error)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 清除所有存储的 token
|
||
func clearTokens() {
|
||
print("🗑️ TokenManager: 清除所有 token")
|
||
KeychainHelper.clearTokens()
|
||
// 清除其他与 token 相关的存储
|
||
UserDefaults.standard.removeObject(forKey: "tokenExpiryDate")
|
||
UserDefaults.standard.synchronize()
|
||
}
|
||
}
|
||
|
||
// MARK: - Token响应模型
|
||
/// 用于解析token刷新接口的响应数据
|
||
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"
|
||
case expiresIn = "expires_in"
|
||
case tokenType = "token_type"
|
||
}
|
||
}
|
||
|
||
// MARK: - 身份验证响应模型
|
||
/// 用于解析身份验证接口的响应数据
|
||
private struct IdentityCheckResponse: Codable {
|
||
/// 是否有效
|
||
let isValid: Bool
|
||
|
||
/// 用户ID(可选)
|
||
let userId: String?
|
||
|
||
/// 过期时间(可选)
|
||
let expiresAt: Date?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case isValid = "is_valid"
|
||
case userId = "user_id"
|
||
case expiresAt = "expires_at"
|
||
}
|
||
}
|
||
|
||
// MARK: - NetworkService扩展
|
||
/// 为NetworkService添加带token验证的请求方法
|
||
extension NetworkService {
|
||
// MARK: - 核心请求方法
|
||
|
||
/// 带token验证的网络请求
|
||
/// - Parameters:
|
||
/// - method: HTTP方法(GET/POST/PUT/DELETE等)
|
||
/// - path: 接口路径
|
||
/// - parameters: 请求参数
|
||
/// - headers: 自定义请求头
|
||
/// - completion: 完成回调
|
||
func requestWithToken<T: Decodable>(
|
||
_ method: String,
|
||
path: String,
|
||
parameters: [String: Any]? = nil,
|
||
headers: [String: String]? = nil,
|
||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||
) {
|
||
// 1. 验证并刷新token
|
||
TokenManager.shared.validateAndRefreshTokenIfNeeded { [weak self] isValid, error in
|
||
guard let self = self else { return }
|
||
|
||
if isValid {
|
||
// 2. token有效,继续发送原始请求
|
||
switch method.uppercased() {
|
||
case "GET":
|
||
self.get(path: path, parameters: parameters, headers: headers, completion: completion)
|
||
case "POST":
|
||
self.post(path: path, parameters: parameters, headers: headers, completion: completion)
|
||
case "PUT":
|
||
self.put(path: path, parameters: parameters, headers: headers, completion: completion)
|
||
case "DELETE":
|
||
self.delete(path: path, parameters: parameters, headers: headers, completion: completion)
|
||
default:
|
||
let error = NSError(
|
||
domain: "NetworkService",
|
||
code: 400,
|
||
userInfo: [NSLocalizedDescriptionKey: "不支持的HTTP方法: \(method)"]
|
||
)
|
||
completion(.failure(.other(error)))
|
||
}
|
||
} else {
|
||
// 3. token无效,返回未授权错误
|
||
let error = error ?? NSError(
|
||
domain: "NetworkService",
|
||
code: 401,
|
||
userInfo: [NSLocalizedDescriptionKey: "未授权访问"]
|
||
)
|
||
|
||
DispatchQueue.main.async {
|
||
completion(.failure(.unauthorized))
|
||
}
|
||
|
||
// 4. 发送登出通知,让应用处理未授权情况
|
||
NotificationCenter.default.post(name: .userDidLogout, object: nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 便捷方法
|
||
|
||
/// 带token验证的GET请求
|
||
func getWithToken<T: Decodable>(
|
||
path: String,
|
||
parameters: [String: Any]? = nil,
|
||
headers: [String: String]? = nil,
|
||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||
) {
|
||
requestWithToken("GET", path: path, parameters: parameters, headers: headers, completion: completion)
|
||
}
|
||
|
||
/// 带token验证的POST请求
|
||
func postWithToken<T: Decodable>(
|
||
path: String,
|
||
parameters: [String: Any]? = nil,
|
||
headers: [String: String]? = nil,
|
||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||
) {
|
||
requestWithToken("POST", path: path, parameters: parameters, headers: headers, completion: completion)
|
||
}
|
||
|
||
/// 带token验证的PUT请求
|
||
func putWithToken<T: Decodable>(
|
||
path: String,
|
||
parameters: [String: Any]? = nil,
|
||
headers: [String: String]? = nil,
|
||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||
) {
|
||
requestWithToken("PUT", path: path, parameters: parameters, headers: headers, completion: completion)
|
||
}
|
||
|
||
/// 带token验证的DELETE请求
|
||
func deleteWithToken<T: Decodable>(
|
||
path: String,
|
||
parameters: [String: Any]? = nil,
|
||
headers: [String: String]? = nil,
|
||
completion: @escaping (Result<T, NetworkError>) -> Void
|
||
) {
|
||
requestWithToken("DELETE", path: path, parameters: parameters, headers: headers, completion: completion)
|
||
}
|
||
}
|
||
|
||
// MARK: - 通知名称
|
||
/// 定义应用中使用的通知名称
|
||
extension Notification.Name {
|
||
/// 用户登出通知
|
||
/// 当token失效或用户主动登出时发送
|
||
static let userDidLogout = Notification.Name("UserDidLogoutNotification")
|
||
}
|