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

@@ -72,7 +72,18 @@ final class ControllerViewModel: NSObject, ObservableObject {
///
@Published var lastRecordingPath: String?
let myDeviceId: String = DeviceUtils.deviceId()
/// ID REGISTER_SUCCESS fromDeviceId
@Published var myDeviceId: String = ""
// MARK: -
@Published var isLoggedIn: Bool = false
@Published var username: String = ""
@Published var loginError: String?
@Published var isLoggingIn: Bool = false
/// accessTokenBearer refreshToken Keychain
private var accessToken: String?
// MARK: - UIKit ViewModel SwiftUI Representable
@@ -102,6 +113,7 @@ final class ControllerViewModel: NSObject, ObservableObject {
override init() {
super.init()
remoteVideoView.delegate = self
restoreLoginState()
selfCodecDecoder.displayLayer = selfCodecView.sampleBufferLayer
selfCodecDecoder.onResolutionUpdate = { [weak self] w, h in
guard let self, self.streamMode == .selfCodec, w > 0, h > 0 else { return }
@@ -129,7 +141,7 @@ final class ControllerViewModel: NSObject, ObservableObject {
// MARK: - /
/// ""
/// ""Bearer token
func requestConnect() {
let target = targetDeviceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !serverUrl.trimmingCharacters(in: .whitespaces).isEmpty else {
@@ -140,10 +152,14 @@ final class ControllerViewModel: NSObject, ObservableObject {
alertMessage = "请输入目标设备 ID"
return
}
guard isLoggedIn else {
alertMessage = "请先在右上角登录账号"
return
}
showAuthSheet = true
}
///
/// accessToken
func connect(authType: AuthType, authValue: String) {
showAuthSheet = false
pendingAuthType = authType
@@ -153,7 +169,7 @@ final class ControllerViewModel: NSObject, ObservableObject {
let signaling = SignalingClient(
serverUrl: serverUrl.trimmingCharacters(in: .whitespaces),
deviceId: myDeviceId)
token: accessToken)
signaling.delegate = self
signalingClient = signaling
signaling.connect()
@@ -277,6 +293,10 @@ final class ControllerViewModel: NSObject, ObservableObject {
fileprivate func startWebRTC() {
guard let signaling = signalingClient else { return }
guard !myDeviceId.isEmpty else {
failConnection("未获取到本机设备ID连接中止")
return
}
statusText = "正在建立 WebRTC 连接..."
let client = WebRTCClient(signaling: signaling, myDeviceId: myDeviceId)
client.delegate = self
@@ -295,6 +315,141 @@ final class ControllerViewModel: NSObject, ObservableObject {
disconnect()
}
// MARK: - /
/// Keychain accessToken / refreshToken
private func restoreLoginState() {
let at = TokenStore.loadAccessToken()
let rt = TokenStore.loadRefreshToken()
if at != nil, rt != nil {
accessToken = at
username = TokenStore.loadUsername() ?? ""
isLoggedIn = true
}
}
/// /api/auth/login
func login(username: String, password: String) async {
isLoggingIn = true
loginError = nil
do {
let data = try await ApiClient.login(username: username, password: password)
guard let at = data["accessToken"] as? String,
let rt = data["refreshToken"] as? String else {
throw ApiError(message: "登录返回缺失")
}
TokenStore.saveAccessToken(at)
TokenStore.saveRefreshToken(rt)
TokenStore.saveUsername(username)
accessToken = at
self.username = username
isLoggedIn = true
} catch {
loginError = (error as? ApiError)?.message ?? error.localizedDescription
}
isLoggingIn = false
}
/// 退
func logout() {
TokenStore.clear()
accessToken = nil
username = ""
isLoggedIn = false
if isConnecting || isControlling {
disconnect()
}
}
/// accessToken refreshToken
private func ensureAccessToken() async throws -> String {
if let existing = accessToken {
do {
_ = try await ApiClient.verify(accessToken: existing)
return existing
} catch {
//
}
}
guard let rt = TokenStore.loadRefreshToken() else {
throw ApiError(message: "无 refreshToken请重新登录")
}
let data = try await ApiClient.refresh(refreshToken: rt)
guard let newAt = data["accessToken"] as? String else {
throw ApiError(message: "刷新失败")
}
TokenStore.saveAccessToken(newAt)
if let newRt = data["refreshToken"] as? String {
TokenStore.saveRefreshToken(newRt)
}
accessToken = newAt
return newAt
}
///
private func refreshAndReconnect() async {
do {
let newAt = try await ensureAccessToken()
accessToken = newAt
DispatchQueue.main.async {
self.signalingClient?.disconnect()
self.signalingClient = nil
// REGISTER_SUCCESS startWebRTC
let signaling = SignalingClient(
serverUrl: self.serverUrl.trimmingCharacters(in: .whitespaces),
token: newAt)
signaling.delegate = self
self.signalingClient = signaling
signaling.connect()
}
} catch {
DispatchQueue.main.async {
self.logout()
self.alertMessage = "令牌刷新失败,请重新登录"
}
}
}
///
private func loadBindings() {
guard let at = accessToken else { return }
Task {
do {
let data = try await ApiClient.bindings(accessToken: at)
guard let list = data["bindings"] as? [[String: Any]], !list.isEmpty else { return }
let uids = list.compactMap { $ -> String? in
if let u = $["deviceUid"] as? String, !u.isEmpty { return u }
return $["deviceId"] as? String
}.filter { !$0.isEmpty }
if !uids.isEmpty {
DispatchQueue.main.async {
self.alertMessage = "已绑定设备: " + uids.joined(separator: ", ")
}
}
} catch { /* */ }
}
}
/// TURN ICE WebRTCClient
private func loadTurnCredentials() async {
guard let at = accessToken else { return }
do {
let data = try await ApiClient.turnCredentials(accessToken: at)
guard let servers = data["iceServers"] as? [[String: Any]], !servers.isEmpty else { return }
var mapped: [[String: Any]] = []
for s in servers {
var entry: [String: Any] = [:]
if let urls = s["urls"] as? String { entry["urls"] = urls }
if let user = s["username"] as? String { entry["username"] = user }
if let cred = s["credential"] as? String { entry["credential"] = cred }
mapped.append(entry)
}
if !mapped.isEmpty {
WebRTCClient.iceServersOverride = mapped
}
} catch { /* TURN */ }
}
// MARK: -
private func startStatsTimer() {
@@ -431,8 +586,7 @@ final class ControllerViewModel: NSObject, ObservableObject {
extension ControllerViewModel: SignalingClientDelegate {
func signalingDidConnect() {
statusText = "信令已连接,正在发起会话..."
startWebRTC()
statusText = "信令已连接,等待注册..."
}
func signalingDidDisconnect() {
@@ -445,7 +599,30 @@ extension ControllerViewModel: SignalingClientDelegate {
failConnection("信令连接失败: \(error)")
}
/// 4001
func signalingTokenExpired() {
Task { await refreshAndReconnect() }
}
/// 线 4003
func signalingForceLogout() {
DispatchQueue.main.async {
self.logout()
self.alertMessage = "账号已在其他位置登录,已强制下线"
}
}
func signaling(didReceive message: SignalMessage) {
// deviceIdCONTROLLER WebRTC Offer
if (message.type ?? "").uppercased() == "REGISTER_SUCCESS",
let from = message.fromDeviceId, !from.isEmpty {
myDeviceId = from
statusText = "已注册 (\(from)),正在发起会话..."
startWebRTC()
loadBindings()
Task { await loadTurnCredentials() }
return
}
switch (message.type ?? "").uppercased() {
case "ANSWER":
if let sdp = message.payloadJSON()?["sdp"] as? String {