wake-ios/wake/Utils/TokenManager.swift
2025-08-21 19:39:09 +08:00

322 lines
12 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
/// Token
///
class TokenManager {
///
static let shared = TokenManager()
/// tokentoken
/// 300token5
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: tokentruefalse
///
/// tokentokentoken
///
/// - Note: tokentoken
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)")
// 200token
isValid = true
print(" TokenManager: 状态码200假设token有效")
}
} else {
// 200token
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? {
// JWTtoken
// 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 = base64 + padding
}
return Data(base64Encoded: base64)
}
/// token
/// - Parameter completion:
/// - success:
/// - error:
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.post(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: -
/// 使
extension Notification.Name {
///
/// token
static let userDidLogout = Notification.Name("UserDidLogoutNotification")
}