feat(controlled): 实现设备激活与安全认证流程

- 添加API客户端、加密存储和provision/token激活逻辑
- WebSocket改用Bearer令牌认证,移除REGISTER请求
- 设备ID改为服务端下发,支持令牌刷新和强制下线处理
- 新增deviceSecret加密存储和accessToken自动刷新
- 更新设备ID获取方式为出厂SN,添加安全存储依赖
This commit is contained in:
2026-08-01 15:13:12 +08:00
parent 376a2c1217
commit 6eb2c7321a
124 changed files with 10535 additions and 862 deletions

View File

@@ -0,0 +1,94 @@
import Foundation
/// HTTP
///
/// - login(username,password) accessToken + refreshTokenses_
/// - refresh(refreshToken) accessToken refreshToken
/// - verify() accessToken
/// - bindings()
/// - turnCredentials() TURN iceServers
struct ApiClient {
/// HTTP Build Setting / Info.plist
static var baseURL: String {
// Info.plist APIBase退 HTTPS
if let v = Bundle.main.object(forInfoDictionaryKey: "APIBase") as? String, !v.isEmpty {
return v
}
return "https://www.ttstd.com"
}
/// accessToken / refreshToken / expireAt
static func login(username: String, password: String) async throws -> [String: Any] {
let body: [String: Any] = ["username": username, "password": password]
return try await post("/api/auth/login", body: body)
}
/// body refreshToken accessToken refreshToken
static func refresh(refreshToken: String) async throws -> [String: Any] {
let body: [String: Any] = ["refreshToken": refreshToken]
return try await post("/api/auth/refresh", body: body)
}
/// accessToken
static func verify(accessToken: String) async throws -> [String: Any] {
return try await get("/api/client/verify", accessToken: accessToken)
}
///
static func bindings(accessToken: String) async throws -> [String: Any] {
return try await get("/api/client/bindings", accessToken: accessToken)
}
/// TURN
static func turnCredentials(accessToken: String) async throws -> [String: Any] {
return try await get("/api/client/turn-credentials", accessToken: accessToken)
}
// MARK: - Private
private static func post(_ path: String, body: [String: Any]) async throws -> [String: Any] {
var req = URLRequest(url: URL(string: baseURL + path)!)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: body)
return try await perform(req)
}
private static func get(_ path: String, accessToken: String) async throws -> [String: Any] {
var req = URLRequest(url: URL(string: baseURL + path)!)
req.httpMethod = "GET"
req.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
return try await perform(req)
}
private static func perform(_ request: URLRequest) async throws -> [String: Any] {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw ApiError(message: "无效响应")
}
if http.statusCode == 401 {
throw ApiError(code: "UNAUTHORIZED", message: "令牌失效", httpCode: 401)
}
guard (200..<300).contains(http.statusCode) else {
let msg = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["error"] as? String
?? "HTTP \(http.statusCode)"
throw ApiError(message: msg)
}
return (try? JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:]
}
}
struct ApiError: Error {
let code: String
let message: String
let httpCode: Int
init(code: String = "ERROR", message: String, httpCode: Int = -1) {
self.code = code
self.message = message
self.httpCode = httpCode
}
var localizedDescription: String { message }
}

View File

@@ -0,0 +1,81 @@
import Foundation
import Security
/// Keychain
///
/// refreshToken ses_ Keychain
/// accessToken 线
enum TokenStore {
private static let service = "com.ttstd.webrtccontroller"
private static let accessKey = "access_token"
private static let refreshKey = "refresh_token"
private static let userKey = "username"
// MARK: - Access Token + Keychain 便
static func saveAccessToken(_ token: String) {
save(key: accessKey, value: token)
}
static func loadAccessToken() -> String? {
load(key: accessKey)
}
// MARK: - Refresh Token Keychain
static func saveRefreshToken(_ token: String) {
save(key: refreshKey, value: token)
}
static func loadRefreshToken() -> String? {
load(key: refreshKey)
}
static func saveUsername(_ name: String) {
save(key: userKey, value: name)
}
static func loadUsername() -> String? {
load(key: userKey)
}
static func clear() {
for k in [accessKey, refreshKey, userKey] {
let query: [String: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: k
]
SecItemDelete(query as CFDictionary)
}
}
// MARK: - Private
private static func save(key: String, value: String) {
//
let query: [String: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: key,
kSecValueData: Data(value.utf8)
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
private static func load(key: String) -> String? {
let query: [String: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: key,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
}