feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑 - WebSocket改用Bearer令牌认证,移除REGISTER请求 - 设备ID改为服务端下发,支持令牌刷新和强制下线处理 - 新增deviceSecret加密存储和accessToken自动刷新 - 更新设备ID获取方式为出厂SN,添加安全存储依赖
This commit is contained in:
@@ -5,17 +5,22 @@ protocol SignalingClientDelegate: AnyObject {
|
||||
func signalingDidDisconnect()
|
||||
func signaling(didFail error: String)
|
||||
func signaling(didReceive message: SignalMessage)
|
||||
/// 令牌失效(关闭码 4001):需刷新令牌后重连。
|
||||
func signalingTokenExpired()
|
||||
/// 强制下线(关闭码 4003):需停止重连并跳登录。
|
||||
func signalingForceLogout()
|
||||
}
|
||||
|
||||
/// 信令 WebSocket 客户端(URLSessionWebSocketTask 实现)。
|
||||
/// 连接成功后自动发送 REGISTER(deviceType = CONTROLLER),
|
||||
/// 所有回调均已切换到主线程。
|
||||
///
|
||||
/// 鉴权方式:通过 URLRequest 在握手请求头携带 `Authorization: Bearer <accessToken>`。
|
||||
/// 不再发送 REGISTER —— 连接由服务端根据令牌身份自动完成,并下发 REGISTER_SUCCESS。
|
||||
final class SignalingClient: NSObject {
|
||||
|
||||
weak var delegate: SignalingClientDelegate?
|
||||
|
||||
private let serverUrl: String
|
||||
private let deviceId: String
|
||||
private let token: String?
|
||||
private var session: URLSession?
|
||||
private var task: URLSessionWebSocketTask?
|
||||
private var manuallyClosed = false
|
||||
@@ -23,10 +28,12 @@ final class SignalingClient: NSObject {
|
||||
private var isOpened = false
|
||||
/// 是否已上报过断开/失败,避免重复回调
|
||||
private var didNotifyClosure = false
|
||||
/// 心跳定时器(每 25s 发送 PING)
|
||||
private var heartbeatTimer: Timer?
|
||||
|
||||
init(serverUrl: String, deviceId: String) {
|
||||
init(serverUrl: String, token: String?) {
|
||||
self.serverUrl = serverUrl
|
||||
self.deviceId = deviceId
|
||||
self.token = token
|
||||
super.init()
|
||||
}
|
||||
|
||||
@@ -38,10 +45,17 @@ final class SignalingClient: NSObject {
|
||||
manuallyClosed = false
|
||||
isOpened = false
|
||||
didNotifyClosure = false
|
||||
|
||||
// 握手携带 Bearer token(URLSessionWebSocketTask 不直接支持自定义头,需用 URLRequest)。
|
||||
var request = URLRequest(url: url)
|
||||
if let token, !token.isEmpty {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = 15
|
||||
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
||||
task = session?.webSocketTask(with: url)
|
||||
task = session?.webSocketTask(with: request)
|
||||
// 接收循环在 didOpenWithProtocol(握手完成)后再启动,
|
||||
// 避免在 socket 未真正连接时调用 receive 触发 Code 57。
|
||||
task?.resume()
|
||||
@@ -50,6 +64,7 @@ final class SignalingClient: NSObject {
|
||||
func disconnect() {
|
||||
manuallyClosed = true
|
||||
isOpened = false
|
||||
stopHeartbeat()
|
||||
task?.cancel(with: .normalClosure, reason: "Disconnecting".data(using: .utf8))
|
||||
task = nil
|
||||
session?.invalidateAndCancel()
|
||||
@@ -71,12 +86,19 @@ final class SignalingClient: NSObject {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func registerDevice() {
|
||||
var msg = SignalMessage()
|
||||
msg.type = "REGISTER"
|
||||
msg.fromDeviceId = deviceId
|
||||
msg.deviceType = "CONTROLLER"
|
||||
send(msg)
|
||||
private func startHeartbeat() {
|
||||
stopHeartbeat()
|
||||
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 25, repeats: true) { [weak self] _ in
|
||||
guard let self, let task = self.task else { return }
|
||||
task.send(.string("{\"type\":\"PING\"}")) { error in
|
||||
if let error { NSLog("[Signaling] ping error: \(error.localizedDescription)") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopHeartbeat() {
|
||||
heartbeatTimer?.invalidate()
|
||||
heartbeatTimer = nil
|
||||
}
|
||||
|
||||
private func receiveLoop() {
|
||||
@@ -99,24 +121,20 @@ final class SignalingClient: NSObject {
|
||||
}
|
||||
|
||||
/// 处理接收失败:区分"正常断开"与"连接失败"。
|
||||
/// Socket 已断开(Code 57 等)视为断开而非致命错误。
|
||||
private func handleReceiveFailure(_ error: Error) {
|
||||
guard !manuallyClosed, !didNotifyClosure else { return }
|
||||
didNotifyClosure = true
|
||||
|
||||
let nsError = error as NSError
|
||||
// NSPOSIXErrorDomain Code 57: Socket is not connected(连接已断开)
|
||||
let isDisconnect = (nsError.domain == NSPOSIXErrorDomain && nsError.code == 57)
|
||||
|| (nsError.domain == NSURLErrorDomain
|
||||
&& (nsError.code == NSURLErrorNetworkConnectionLost
|
||||
|| nsError.code == NSURLErrorCancelled))
|
||||
|| nsError.code == NSURLErrorCancelled))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if self.isOpened || isDisconnect {
|
||||
// 连接已建立过后再断开,按"断开"处理
|
||||
self.delegate?.signalingDidDisconnect()
|
||||
} else {
|
||||
// 从未成功建立连接,按"连接失败"处理
|
||||
self.delegate?.signaling(didFail: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
@@ -137,9 +155,10 @@ extension SignalingClient: URLSessionWebSocketDelegate {
|
||||
webSocketTask: URLSessionWebSocketTask,
|
||||
didOpenWithProtocol protocol: String?) {
|
||||
isOpened = true
|
||||
// 握手完成后再启动接收循环,避免 socket 未连接时 receive 报错
|
||||
// 握手完成后再启动接收循环,避免 socket 未连接时 receive 报错。
|
||||
// 不再发送 REGISTER,服务端根据 Bearer 令牌自动注册。
|
||||
receiveLoop()
|
||||
registerDevice()
|
||||
startHeartbeat()
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.signalingDidConnect()
|
||||
}
|
||||
@@ -150,8 +169,19 @@ extension SignalingClient: URLSessionWebSocketDelegate {
|
||||
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
|
||||
reason: Data?) {
|
||||
isOpened = false
|
||||
stopHeartbeat()
|
||||
guard !manuallyClosed, !didNotifyClosure else { return }
|
||||
didNotifyClosure = true
|
||||
|
||||
let code = Int(closeCode.rawValue)
|
||||
if code == 4001 {
|
||||
DispatchQueue.main.async { self.delegate?.signalingTokenExpired() }
|
||||
return
|
||||
}
|
||||
if code == 4003 {
|
||||
DispatchQueue.main.async { self.delegate?.signalingForceLogout() }
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.signalingDidDisconnect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user