feat: 更新项目配置和功能模块
- 修改Package.swift以支持iOS 15和macOS 12。 - 更新swift-tca-architecture-guidelines.mdc中的alwaysApply设置为false。 - 注释掉AppDelegate中的NIMSDK导入,移除不再使用的NIMConfigurationManager和NIMSessionManager文件。 - 添加新的API相关文件,包括EMailLoginFeature、IDLoginFeature和相关视图,增强登录功能。 - 更新APIConstants和APIEndpoints以反映新的API路径。 - 添加本地化支持文件,包含英文和中文简体的本地化字符串。 - 新增字体管理和安全工具类,支持AES和DES加密。 - 更新Xcode项目配置,调整版本号和启动画面设置。
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
|
||||
| 环境 | 地址 | 说明 |
|
||||
|------|------|------|
|
||||
| 生产环境 | `https://api.hfighting.com` | 正式服务器 |
|
||||
| 生产环境 | `https://api.epartylive.com` | 正式服务器 |
|
||||
| 测试环境 | `http://beta.api.molistar.xyz` | 开发测试服务器 |
|
||||
| 图片服务 | `https://image.hfighting.com` | 静态资源服务器 |
|
||||
|
||||
@@ -177,4 +177,4 @@ YuMi iOS 项目的 API 架构设计了完整的网络请求体系,包含:
|
||||
- 🛠️ **开发支持**: 环境切换、错误追踪、调试日志
|
||||
- 🏗️ **架构清晰**: 模块化设计、统一管理、易于维护
|
||||
|
||||
这种设计确保了 API 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。
|
||||
这种设计确保了 API 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。
|
||||
|
@@ -3,17 +3,13 @@ import Foundation
|
||||
/// API 常量定义
|
||||
///
|
||||
/// 集中管理 API 相关的常量值,包括:
|
||||
/// - 服务器地址
|
||||
/// - 通用请求头
|
||||
/// - API 端点路径
|
||||
/// - 通用参数
|
||||
///
|
||||
/// 注意:此文件与 APIConfiguration 有部分重复,
|
||||
/// 注意:baseURL已统一到AppConfig中管理
|
||||
/// 建议后续重构时统一到 APIConfiguration 中
|
||||
enum APIConstants {
|
||||
// MARK: - Base URLs
|
||||
/// 测试环境服务器地址
|
||||
static let baseURL = "http://beta.api.molistar.xyz"
|
||||
|
||||
// MARK: - Common Headers
|
||||
/// 通用请求头配置
|
||||
@@ -34,7 +30,7 @@ enum APIConstants {
|
||||
/// 客户端初始化接口
|
||||
static let clientInit = "/client/init"
|
||||
/// 用户登录接口
|
||||
static let login = "/user/login"
|
||||
static let login = "/oauth/token"
|
||||
}
|
||||
|
||||
// MARK: - Common Parameters
|
||||
|
@@ -16,8 +16,11 @@ import Foundation
|
||||
enum APIEndpoint: String, CaseIterable {
|
||||
case config = "/client/config"
|
||||
case configInit = "/client/init"
|
||||
case login = "/auth/login"
|
||||
// 可以继续添加其他端点
|
||||
case login = "/oauth/token"
|
||||
case ticket = "/oauth/ticket"
|
||||
// Web 页面路径
|
||||
case userAgreement = "/modules/rule/protocol.html"
|
||||
case privacyPolicy = "/modules/rule/privacy-wap.html"
|
||||
|
||||
var path: String {
|
||||
return self.rawValue
|
||||
@@ -39,10 +42,38 @@ enum APIEndpoint: String, CaseIterable {
|
||||
/// - 防止资源超限的保护机制
|
||||
/// - 自动添加认证和设备信息头部
|
||||
struct APIConfiguration {
|
||||
static let baseURL = "http://beta.api.molistar.xyz"
|
||||
static var baseURL: String { AppConfig.baseURL }
|
||||
static let timeout: TimeInterval = 30.0
|
||||
static let maxDataSize: Int = 50 * 1024 * 1024 // 50MB 限制,防止资源超限
|
||||
|
||||
/// 构建完整的 URL
|
||||
/// - Parameter endpoint: API 端点
|
||||
/// - Returns: 完整的 URL 字符串
|
||||
static func fullURL(for endpoint: APIEndpoint) -> String {
|
||||
return baseURL + endpoint.path
|
||||
}
|
||||
|
||||
/// 构建完整的 URL 对象
|
||||
/// - Parameter endpoint: API 端点
|
||||
/// - Returns: URL 对象,如果构建失败返回 nil
|
||||
static func url(for endpoint: APIEndpoint) -> URL? {
|
||||
return URL(string: fullURL(for: endpoint))
|
||||
}
|
||||
|
||||
/// 构建Web页面的完整 URL
|
||||
/// - Parameter endpoint: API 端点
|
||||
/// - Returns: 完整的Web页面 URL 字符串
|
||||
static func fullWebURL(for endpoint: APIEndpoint) -> String {
|
||||
return baseURL + AppConfig.webPathPrefix + endpoint.path
|
||||
}
|
||||
|
||||
/// 构建Web页面的完整 URL 对象
|
||||
/// - Parameter endpoint: API 端点
|
||||
/// - Returns: Web页面 URL 对象,如果构建失败返回 nil
|
||||
static func webURL(for endpoint: APIEndpoint) -> URL? {
|
||||
return URL(string: fullWebURL(for: endpoint))
|
||||
}
|
||||
|
||||
/// 默认请求头配置
|
||||
///
|
||||
/// 返回所有 API 请求都需要的基础请求头,包括:
|
||||
@@ -58,7 +89,8 @@ struct APIConfiguration {
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip, br",
|
||||
"Accept-Language": Locale.current.languageCode ?? "en",
|
||||
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||||
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
|
||||
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 16.4; Scale/2.00)"
|
||||
]
|
||||
|
||||
// 添加用户认证相关 headers(如果存在)
|
||||
|
@@ -118,7 +118,7 @@ struct BaseRequest: Codable {
|
||||
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||||
|
||||
// 应用名称
|
||||
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "yana"
|
||||
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "eparty"
|
||||
|
||||
// 网络类型检测(WiFi=2, 蜂窝网络=1)
|
||||
self.netType = NetworkTypeDetector.getCurrentNetworkType()
|
||||
@@ -131,7 +131,7 @@ struct BaseRequest: Codable {
|
||||
|
||||
// 渠道信息
|
||||
#if DEBUG
|
||||
self.channel = "TestFlight"
|
||||
self.channel = "molistar_enterprise"
|
||||
#else
|
||||
self.channel = "appstore"
|
||||
#endif
|
||||
@@ -186,9 +186,10 @@ struct BaseRequest: Codable {
|
||||
}
|
||||
|
||||
// 3. 按 key 升序排序并拼接
|
||||
// 拼接格式 "key0=value0&key1=value1&key2=value2"
|
||||
let sortedKeys = filteredParams.keys.sorted()
|
||||
let paramString = sortedKeys.map { key in
|
||||
"\(key)=\(filteredParams[key] ?? "")"
|
||||
"\(key)=\(String(describing: filteredParams[key] ?? ""))"
|
||||
}.joined(separator: "&")
|
||||
|
||||
// 4. 添加密钥
|
||||
@@ -205,7 +206,7 @@ struct NetworkTypeDetector {
|
||||
static func getCurrentNetworkType() -> Int {
|
||||
// WiFi = 2, 蜂窝网络 = 1
|
||||
// 这里是简化实现,实际应该检测网络状态
|
||||
return 1 // 默认蜂窝网络
|
||||
return 2 // 默认蜂窝网络
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,16 +225,136 @@ struct CarrierInfoManager {
|
||||
|
||||
// MARK: - User Info Manager (for Headers)
|
||||
struct UserInfoManager {
|
||||
static func getCurrentUserId() -> String? {
|
||||
// 从存储中获取当前用户 ID
|
||||
// 实际实现应该从 AccountInfoStorage 或类似的地方获取
|
||||
return nil
|
||||
private static let userDefaults = UserDefaults.standard
|
||||
|
||||
// MARK: - Storage Keys
|
||||
private enum StorageKeys {
|
||||
static let userId = "user_id"
|
||||
static let accessToken = "access_token"
|
||||
static let ticket = "user_ticket"
|
||||
static let userInfo = "user_info"
|
||||
}
|
||||
|
||||
// MARK: - User ID Management
|
||||
static func getCurrentUserId() -> String? {
|
||||
return userDefaults.string(forKey: StorageKeys.userId)
|
||||
}
|
||||
|
||||
static func saveUserId(_ userId: String) {
|
||||
userDefaults.set(userId, forKey: StorageKeys.userId)
|
||||
userDefaults.synchronize()
|
||||
print("💾 保存用户ID: \(userId)")
|
||||
}
|
||||
|
||||
// MARK: - Access Token Management
|
||||
static func getAccessToken() -> String? {
|
||||
return userDefaults.string(forKey: StorageKeys.accessToken)
|
||||
}
|
||||
|
||||
static func saveAccessToken(_ accessToken: String) {
|
||||
userDefaults.set(accessToken, forKey: StorageKeys.accessToken)
|
||||
userDefaults.synchronize()
|
||||
print("💾 保存 Access Token")
|
||||
}
|
||||
|
||||
// MARK: - Ticket Management (内存存储)
|
||||
private static var currentTicket: String?
|
||||
|
||||
static func getCurrentUserTicket() -> String? {
|
||||
// 从存储中获取当前用户认证票据
|
||||
// 实际实现应该从 AccountInfoStorage 或类似的地方获取
|
||||
return nil
|
||||
return currentTicket
|
||||
}
|
||||
|
||||
static func saveTicket(_ ticket: String) {
|
||||
currentTicket = ticket
|
||||
print("💾 保存 Ticket 到内存")
|
||||
}
|
||||
|
||||
static func clearTicket() {
|
||||
currentTicket = nil
|
||||
print("🗑️ 清除 Ticket")
|
||||
}
|
||||
|
||||
// MARK: - User Info Management
|
||||
static func saveUserInfo(_ userInfo: UserInfo) {
|
||||
do {
|
||||
let data = try JSONEncoder().encode(userInfo)
|
||||
userDefaults.set(data, forKey: StorageKeys.userInfo)
|
||||
userDefaults.synchronize()
|
||||
|
||||
// 同时保存用户ID
|
||||
if let userId = userInfo.userId {
|
||||
saveUserId(userId)
|
||||
}
|
||||
|
||||
print("💾 保存用户信息成功")
|
||||
} catch {
|
||||
print("❌ 保存用户信息失败: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
static func getUserInfo() -> UserInfo? {
|
||||
guard let data = userDefaults.data(forKey: StorageKeys.userInfo) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
return try JSONDecoder().decode(UserInfo.self, from: data)
|
||||
} catch {
|
||||
print("❌ 解析用户信息失败: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Complete Authentication Data Management
|
||||
/// 保存完整的认证信息(OAuth Token + Ticket + 用户信息)
|
||||
static func saveCompleteAuthenticationData(
|
||||
accessToken: String,
|
||||
ticket: String,
|
||||
uid: Int?, // 修改:从String?改为Int?
|
||||
userInfo: UserInfo?
|
||||
) {
|
||||
saveAccessToken(accessToken)
|
||||
saveTicket(ticket)
|
||||
|
||||
if let uid = uid {
|
||||
saveUserId("\(uid)") // 转换为字符串保存
|
||||
}
|
||||
|
||||
if let userInfo = userInfo {
|
||||
saveUserInfo(userInfo)
|
||||
}
|
||||
|
||||
print("✅ 完整认证信息保存成功")
|
||||
}
|
||||
|
||||
/// 检查是否有有效的认证信息
|
||||
static func hasValidAuthentication() -> Bool {
|
||||
return getAccessToken() != nil && getCurrentUserTicket() != nil
|
||||
}
|
||||
|
||||
/// 清除所有认证信息
|
||||
static func clearAllAuthenticationData() {
|
||||
userDefaults.removeObject(forKey: StorageKeys.userId)
|
||||
userDefaults.removeObject(forKey: StorageKeys.accessToken)
|
||||
userDefaults.removeObject(forKey: StorageKeys.userInfo)
|
||||
clearTicket()
|
||||
userDefaults.synchronize()
|
||||
|
||||
print("🗑️ 清除所有认证信息")
|
||||
}
|
||||
|
||||
/// 尝试恢复 Ticket(用于应用重启后)
|
||||
static func restoreTicketIfNeeded() async -> Bool {
|
||||
guard let accessToken = getAccessToken(),
|
||||
getCurrentUserTicket() == nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
print("🔄 尝试使用 Access Token 恢复 Ticket...")
|
||||
|
||||
// 这里需要注入 APIService 依赖,暂时返回 false
|
||||
// 实际实现中应该调用 TicketHelper.createTicketRequest
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +388,7 @@ protocol APIRequestProtocol {
|
||||
var queryParameters: [String: String]? { get }
|
||||
var bodyParameters: [String: Any]? { get }
|
||||
var headers: [String: String]? { get }
|
||||
var customHeaders: [String: String]? { get } // 新增:自定义请求头
|
||||
var timeout: TimeInterval { get }
|
||||
var includeBaseParameters: Bool { get }
|
||||
}
|
||||
@@ -275,6 +397,7 @@ extension APIRequestProtocol {
|
||||
var timeout: TimeInterval { 30.0 }
|
||||
var includeBaseParameters: Bool { true }
|
||||
var headers: [String: String]? { nil }
|
||||
var customHeaders: [String: String]? { nil } // 新增:默认实现
|
||||
}
|
||||
|
||||
// MARK: - Generic API Response
|
||||
@@ -285,19 +408,5 @@ struct APIResponse<T: Codable>: Codable {
|
||||
let code: Int?
|
||||
}
|
||||
|
||||
// MARK: - String MD5 Extension
|
||||
extension String {
|
||||
func md5() -> String {
|
||||
let data = Data(self.utf8)
|
||||
let hash = data.withUnsafeBytes { bytes -> [UInt8] in
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
|
||||
CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
|
||||
return hash
|
||||
}
|
||||
return hash.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
// 需要导入 CommonCrypto
|
||||
import CommonCrypto
|
||||
// 注意:String+MD5 扩展已移至 Utils/Extensions/String+MD5.swift
|
||||
|
||||
|
@@ -93,6 +93,11 @@ struct LiveAPIService: APIServiceProtocol {
|
||||
headers.merge(customHeaders) { _, new in new }
|
||||
}
|
||||
|
||||
// 添加自定义请求头支持
|
||||
if let additionalHeaders = request.customHeaders {
|
||||
headers.merge(additionalHeaders) { _, new in new }
|
||||
}
|
||||
|
||||
for (key, value) in headers {
|
||||
urlRequest.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
236
yana/APIs/LoginModels.swift
Normal file
236
yana/APIs/LoginModels.swift
Normal file
@@ -0,0 +1,236 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - ID Login Request Model
|
||||
struct IDLoginAPIRequest: APIRequestProtocol {
|
||||
typealias Response = IDLoginResponse
|
||||
|
||||
let endpoint = APIEndpoint.login.path // 使用枚举定义的登录端点
|
||||
let method: HTTPMethod = .POST
|
||||
let includeBaseParameters = true
|
||||
let queryParameters: [String: String]?
|
||||
let bodyParameters: [String: Any]? = nil
|
||||
let timeout: TimeInterval = 30.0
|
||||
|
||||
/// 初始化ID登录请求
|
||||
/// - Parameters:
|
||||
/// - phone: DES加密后的用户ID/手机号
|
||||
/// - password: DES加密后的密码
|
||||
/// - clientSecret: 客户端密钥,固定为"uyzjdhds"
|
||||
/// - version: 版本号,固定为"1"
|
||||
/// - clientId: 客户端ID,固定为"erban-client"
|
||||
/// - grantType: 授权类型,固定为"password"
|
||||
init(phone: String, password: String, clientSecret: String = "uyzjdhds", version: String = "1", clientId: String = "erban-client", grantType: String = "password") {
|
||||
self.queryParameters = [
|
||||
"phone": phone,
|
||||
"password": password,
|
||||
"client_secret": clientSecret,
|
||||
"version": version,
|
||||
"client_id": clientId,
|
||||
"grant_type": grantType
|
||||
];
|
||||
// self.bodyParameters = [
|
||||
// "phone": phone,
|
||||
// "password": password,
|
||||
// "client_secret": clientSecret,
|
||||
// "version": version,
|
||||
// "client_id": clientId,
|
||||
// "grant_type": grantType
|
||||
// ]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ID Login Response Model
|
||||
struct IDLoginResponse: Codable, Equatable {
|
||||
let status: String?
|
||||
let message: String?
|
||||
let code: Int?
|
||||
let data: IDLoginData?
|
||||
|
||||
/// 是否登录成功
|
||||
var isSuccess: Bool {
|
||||
return code == 200 || status?.lowercased() == "success"
|
||||
}
|
||||
|
||||
/// 错误消息(如果有)
|
||||
var errorMessage: String {
|
||||
return message ?? "登录失败,请重试"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ID Login Data Model
|
||||
struct IDLoginData: Codable, Equatable {
|
||||
let accessToken: String?
|
||||
let refreshToken: String?
|
||||
let tokenType: String?
|
||||
let expiresIn: Int?
|
||||
let scope: String?
|
||||
let userInfo: UserInfo?
|
||||
let uid: Int? // 修改:从String?改为Int?以匹配API返回
|
||||
let netEaseToken: String? // 新增:网易云token
|
||||
let jti: String? // 新增:JWT token identifier
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case refreshToken = "refresh_token"
|
||||
case tokenType = "token_type"
|
||||
case expiresIn = "expires_in"
|
||||
case scope
|
||||
case userInfo = "user_info"
|
||||
case uid
|
||||
case netEaseToken
|
||||
case jti
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Info Model
|
||||
struct UserInfo: Codable, Equatable {
|
||||
let userId: String?
|
||||
let username: String?
|
||||
let nickname: String?
|
||||
let avatar: String?
|
||||
let email: String?
|
||||
let phone: String?
|
||||
let status: String?
|
||||
let createTime: String?
|
||||
let updateTime: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case userId = "user_id"
|
||||
case username
|
||||
case nickname
|
||||
case avatar
|
||||
case email
|
||||
case phone
|
||||
case status
|
||||
case createTime = "create_time"
|
||||
case updateTime = "update_time"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Login Helper
|
||||
struct LoginHelper {
|
||||
|
||||
/// 创建ID登录请求
|
||||
/// 这个方法会自动处理DES加密
|
||||
/// - Parameters:
|
||||
/// - userID: 原始用户ID
|
||||
/// - password: 原始密码
|
||||
/// - Returns: 配置好的API请求,如果加密失败返回nil
|
||||
static func createIDLoginRequest(userID: String, password: String) -> IDLoginAPIRequest? {
|
||||
// 使用DES加密ID和密码
|
||||
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
|
||||
|
||||
guard let encryptedID = DESEncrypt.encryptUseDES(userID, key: encryptionKey),
|
||||
let encryptedPassword = DESEncrypt.encryptUseDES(password, key: encryptionKey) else {
|
||||
print("❌ DES加密失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
print("🔐 DES加密成功")
|
||||
print(" 原始ID: \(userID)")
|
||||
print(" 加密后ID: \(encryptedID)")
|
||||
print(" 原始密码: \(password)")
|
||||
print(" 加密后密码: \(encryptedPassword)")
|
||||
|
||||
return IDLoginAPIRequest(
|
||||
phone: userID,
|
||||
password: encryptedPassword
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Ticket API Models
|
||||
|
||||
/// Ticket 请求结构体
|
||||
struct TicketAPIRequest: APIRequestProtocol {
|
||||
typealias Response = TicketResponse
|
||||
|
||||
let endpoint = "/oauth/ticket"
|
||||
let method: HTTPMethod = .POST
|
||||
let includeBaseParameters = true
|
||||
let queryParameters: [String: String]?
|
||||
let bodyParameters: [String: Any]? = nil
|
||||
let timeout: TimeInterval = 30.0
|
||||
let customHeaders: [String: String]?
|
||||
|
||||
/// 初始化 Ticket 请求
|
||||
/// - Parameters:
|
||||
/// - accessToken: OAuth 访问令牌
|
||||
/// - issueType: 签发类型,固定为"multi"
|
||||
/// - uid: 用户唯一标识,用于添加到请求头
|
||||
init(accessToken: String, issueType: String = "multi", uid: Int? = nil) {
|
||||
self.queryParameters = [
|
||||
"access_token": accessToken,
|
||||
"issue_type": issueType
|
||||
]
|
||||
|
||||
// 设置自定义请求头
|
||||
var headers: [String: String] = [:]
|
||||
if let uid = uid {
|
||||
headers["pub_uid"] = "\(uid)" // 转换为字符串
|
||||
}
|
||||
self.customHeaders = headers.isEmpty ? nil : headers
|
||||
}
|
||||
}
|
||||
|
||||
/// Ticket 响应结构体
|
||||
struct TicketResponse: Codable, Equatable {
|
||||
let code: Int?
|
||||
let message: String?
|
||||
let data: TicketData?
|
||||
|
||||
/// 是否获取成功
|
||||
var isSuccess: Bool {
|
||||
return code == 200
|
||||
}
|
||||
|
||||
/// 错误消息(如果有)
|
||||
var errorMessage: String {
|
||||
return message ?? "Ticket 获取失败,请重试"
|
||||
}
|
||||
|
||||
/// 获取 Ticket 字符串
|
||||
var ticket: String? {
|
||||
return data?.tickets?.first?.ticket
|
||||
}
|
||||
}
|
||||
|
||||
/// Ticket 数据结构体
|
||||
struct TicketData: Codable, Equatable {
|
||||
let tickets: [TicketInfo]?
|
||||
}
|
||||
|
||||
/// Ticket 信息结构体
|
||||
struct TicketInfo: Codable, Equatable {
|
||||
let ticket: String?
|
||||
}
|
||||
|
||||
// MARK: - Ticket Helper
|
||||
struct TicketHelper {
|
||||
|
||||
/// 创建 Ticket 请求
|
||||
/// - Parameters:
|
||||
/// - accessToken: OAuth 访问令牌
|
||||
/// - uid: 用户唯一标识
|
||||
/// - Returns: 配置好的 Ticket API 请求
|
||||
static func createTicketRequest(accessToken: String, uid: Int?) -> TicketAPIRequest {
|
||||
return TicketAPIRequest(accessToken: accessToken, uid: uid)
|
||||
}
|
||||
|
||||
/// 调试打印 Ticket 请求信息
|
||||
/// - Parameters:
|
||||
/// - accessToken: OAuth 访问令牌
|
||||
/// - uid: 用户唯一标识
|
||||
static func debugTicketRequest(accessToken: String, uid: Int?) {
|
||||
print("🎫 Ticket 请求调试信息")
|
||||
print(" AccessToken: \(accessToken)")
|
||||
print(" UID: \(uid?.description ?? "nil")")
|
||||
print(" Endpoint: /oauth/ticket")
|
||||
print(" Method: POST")
|
||||
print(" Headers: pub_uid = \(uid?.description ?? "nil")")
|
||||
print(" Parameters: access_token=\(accessToken), issue_type=multi")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 兼容旧的LoginResponse(如果需要)
|
||||
typealias LoginResponse = IDLoginResponse
|
262
yana/APIs/oauth flow.md
Normal file
262
yana/APIs/oauth flow.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# OAuth/Ticket 认证系统 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了 YuMi 应用中 OAuth 认证和 Ticket 会话管理的完整流程。系统采用两阶段认证机制:
|
||||
1. **OAuth 阶段**:用户登录获取 `access_token`
|
||||
2. **Ticket 阶段**:使用 `access_token` 获取业务会话 `ticket`
|
||||
|
||||
## 认证流程架构
|
||||
|
||||
### 核心组件
|
||||
- **AccountInfoStorage**: 负责账户信息和 ticket 的本地存储
|
||||
- **HttpRequestHelper**: 网络请求管理,自动添加认证头
|
||||
- **Api+Login**: 登录相关 API 接口
|
||||
- **Api+Main**: Ticket 获取相关 API 接口
|
||||
|
||||
### 认证数据模型
|
||||
|
||||
#### AccountModel
|
||||
```objc
|
||||
@interface AccountModel : PIBaseModel
|
||||
@property (nonatomic, assign) NSString *uid; // 用户唯一标识
|
||||
@property (nonatomic, copy) NSString *jti; // JWT ID
|
||||
@property (nonatomic, copy) NSString *token_type; // Token 类型
|
||||
@property (nonatomic, copy) NSString *refresh_token; // 刷新令牌
|
||||
@property (nonatomic, copy) NSString *netEaseToken; // 网易云信令牌
|
||||
@property (nonatomic, copy) NSString *access_token; // OAuth 访问令牌
|
||||
@property (nonatomic, assign) NSNumber *expires_in; // 过期时间
|
||||
@end
|
||||
```
|
||||
|
||||
## API 接口详情
|
||||
|
||||
### 1. OAuth 登录接口
|
||||
|
||||
#### 1.1 手机验证码登录
|
||||
```objc
|
||||
+ (void)loginWithCode:(HttpRequestHelperCompletion)completion
|
||||
phone:(NSString *)phone
|
||||
code:(NSString *)code
|
||||
client_secret:(NSString *)client_secret
|
||||
version:(NSString *)version
|
||||
client_id:(NSString *)client_id
|
||||
grant_type:(NSString *)grant_type
|
||||
phoneAreaCode:(NSString *)phoneAreaCode;
|
||||
```
|
||||
|
||||
**接口路径**: `POST /oauth/token`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| phone | String | 是 | 手机号(DES加密) |
|
||||
| code | String | 是 | 验证码 |
|
||||
| client_secret | String | 是 | 客户端密钥,固定值:"uyzjdhds" |
|
||||
| version | String | 是 | 版本号,固定值:"1" |
|
||||
| client_id | String | 是 | 客户端ID,固定值:"erban-client" |
|
||||
| grant_type | String | 是 | 授权类型,验证码登录为:"sms_code" |
|
||||
| phoneAreaCode | String | 是 | 手机区号 |
|
||||
|
||||
**返回数据**: AccountModel 对象
|
||||
|
||||
#### 1.2 手机密码登录
|
||||
```objc
|
||||
+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion
|
||||
phone:(NSString *)phone
|
||||
password:(NSString *)password
|
||||
client_secret:(NSString *)client_secret
|
||||
version:(NSString *)version
|
||||
client_id:(NSString *)client_id
|
||||
grant_type:(NSString *)grant_type;
|
||||
```
|
||||
|
||||
**接口路径**: `POST /oauth/token`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| phone | String | 是 | 手机号(DES加密) |
|
||||
| password | String | 是 | 密码(DES加密) |
|
||||
| client_secret | String | 是 | 客户端密钥 |
|
||||
| version | String | 是 | 版本号 |
|
||||
| client_id | String | 是 | 客户端ID |
|
||||
| grant_type | String | 是 | 授权类型,密码登录为:"password" |
|
||||
|
||||
#### 1.3 第三方登录
|
||||
```objc
|
||||
+ (void)loginWithThirdPart:(HttpRequestHelperCompletion)completion
|
||||
openid:(NSString *)openid
|
||||
unionid:(NSString *)unionid
|
||||
access_token:(NSString *)access_token
|
||||
type:(NSString *)type;
|
||||
```
|
||||
|
||||
**接口路径**: `POST /acc/third/login`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| openid | String | 是 | 第三方平台用户唯一标识 |
|
||||
| unionid | String | 是 | 第三方平台联合ID |
|
||||
| access_token | String | 是 | 第三方平台访问令牌 |
|
||||
| type | String | 是 | 第三方平台类型(1:Apple, 2:Facebook, 3:Google等) |
|
||||
|
||||
### 2. Ticket 获取接口
|
||||
|
||||
#### 2.1 获取 Ticket
|
||||
```objc
|
||||
+ (void)requestTicket:(HttpRequestHelperCompletion)completion
|
||||
access_token:(NSString *)accessToken
|
||||
issue_type:(NSString *)issueType;
|
||||
```
|
||||
|
||||
**接口路径**: `POST /oauth/ticket`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| access_token | String | 是 | OAuth 登录获取的访问令牌 |
|
||||
| issue_type | String | 是 | 签发类型,固定值:"multi" |
|
||||
|
||||
**返回数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"tickets": [
|
||||
{
|
||||
"ticket": "eyJhbGciOiJIUzI1NiJ9..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. HTTP 请求头配置
|
||||
|
||||
所有业务 API 请求都会自动添加以下请求头:
|
||||
|
||||
```objc
|
||||
// 在 HttpRequestHelper 中自动配置
|
||||
- (void)setupHeader {
|
||||
AFHTTPSessionManager *client = [HttpRequestHelper requestManager];
|
||||
|
||||
// 用户ID头
|
||||
if ([[AccountInfoStorage instance] getUid].length > 0) {
|
||||
[client.requestSerializer setValue:[[AccountInfoStorage instance] getUid]
|
||||
forHTTPHeaderField:@"pub_uid"];
|
||||
}
|
||||
|
||||
// Ticket 认证头
|
||||
if ([[AccountInfoStorage instance] getTicket].length > 0) {
|
||||
[client.requestSerializer setValue:[[AccountInfoStorage instance] getTicket]
|
||||
forHTTPHeaderField:@"pub_ticket"];
|
||||
}
|
||||
|
||||
// 其他公共头
|
||||
[client.requestSerializer setValue:[NSBundle uploadLanguageText]
|
||||
forHTTPHeaderField:@"Accept-Language"];
|
||||
[client.requestSerializer setValue:PI_App_Version
|
||||
forHTTPHeaderField:@"App-Version"];
|
||||
}
|
||||
```
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 完整登录流程示例
|
||||
|
||||
```objc
|
||||
// 1. 用户登录获取 access_token
|
||||
[Api loginWithCode:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
if (code == 200) {
|
||||
// 保存账户信息
|
||||
AccountModel *accountModel = [AccountModel modelWithDictionary:data.data];
|
||||
[[AccountInfoStorage instance] saveAccountInfo:accountModel];
|
||||
|
||||
// 2. 使用 access_token 获取 ticket
|
||||
[Api requestTicket:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
if (code == 200) {
|
||||
NSArray *tickets = [data.data valueForKey:@"tickets"];
|
||||
NSString *ticket = [tickets[0] valueForKey:@"ticket"];
|
||||
|
||||
// 保存 ticket
|
||||
[[AccountInfoStorage instance] saveTicket:ticket];
|
||||
|
||||
// 3. 登录成功,可以进行业务操作
|
||||
[self navigateToMainPage];
|
||||
}
|
||||
} access_token:accountModel.access_token issue_type:@"multi"];
|
||||
}
|
||||
} phone:encryptedPhone
|
||||
code:verificationCode
|
||||
client_secret:@"uyzjdhds"
|
||||
version:@"1"
|
||||
client_id:@"erban-client"
|
||||
grant_type:@"sms_code"
|
||||
phoneAreaCode:areaCode];
|
||||
```
|
||||
|
||||
### 自动登录流程
|
||||
|
||||
```objc
|
||||
- (void)autoLogin {
|
||||
// 检查本地是否有账户信息
|
||||
AccountModel *accountModel = [[AccountInfoStorage instance] getCurrentAccountInfo];
|
||||
if (accountModel == nil || accountModel.access_token == nil) {
|
||||
[self tokenInvalid]; // 跳转到登录页
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有有效的 ticket
|
||||
if ([[AccountInfoStorage instance] getTicket].length > 0) {
|
||||
[[self getView] autoLoginSuccess];
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 access_token 重新获取 ticket
|
||||
[Api requestTicket:^(BaseModel * _Nonnull data) {
|
||||
NSArray *tickets = [data.data valueForKey:@"tickets"];
|
||||
NSString *ticket = [tickets[0] valueForKey:@"ticket"];
|
||||
[[AccountInfoStorage instance] saveTicket:ticket];
|
||||
[[self getView] autoLoginSuccess];
|
||||
} fail:^(NSInteger code, NSString * _Nullable msg) {
|
||||
[self logout]; // ticket 获取失败,重新登录
|
||||
} access_token:accountModel.access_token issue_type:@"multi"];
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 401 未授权错误
|
||||
当接收到 401 状态码时,系统会自动处理:
|
||||
|
||||
```objc
|
||||
// 在 HttpRequestHelper 中
|
||||
if (response && response.statusCode == 401) {
|
||||
failure(response.statusCode, YMLocalizedString(@"HttpRequestHelper7"));
|
||||
// 通常需要重新登录
|
||||
}
|
||||
```
|
||||
|
||||
### Ticket 过期处理
|
||||
- Ticket 过期时服务器返回 401 错误
|
||||
- 客户端应该使用保存的 `access_token` 重新获取 ticket
|
||||
- 如果 `access_token` 也过期,则需要用户重新登录
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **数据加密**: 敏感信息(手机号、密码)使用 DES 加密传输
|
||||
2. **本地存储**:
|
||||
- `access_token` 存储在文件系统中
|
||||
- `ticket` 存储在内存中,应用重启需重新获取
|
||||
3. **请求头**: 所有业务请求自动携带 `pub_uid` 和 `pub_ticket` 头
|
||||
4. **错误处理**: 建立完善的 401 错误重试机制
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `YuMi/Structure/MVP/Model/AccountInfoStorage.h/m` - 账户信息存储管理
|
||||
- `YuMi/Modules/YMLogin/Api/Api+Login.h/m` - 登录相关接口
|
||||
- `YuMi/Modules/YMTabbar/Api/Api+Main.h/m` - Ticket 获取接口
|
||||
- `YuMi/Network/HttpRequestHelper.h/m` - 网络请求管理
|
||||
- `YuMi/Structure/MVP/Model/AccountModel.h/m` - 账户数据模型
|
1
yana/APIs/oauth flow.svg
Normal file
1
yana/APIs/oauth flow.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user