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

@@ -4,10 +4,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'api/api_client.dart';
import 'config/ice_servers.dart';
import 'controller/remote_controller.dart';
import 'utils/control_commands.dart';
import 'utils/device_utils.dart';
import 'webrtc/self_codec_decoder.dart';
import 'webrtc/video_recorder.dart';
import 'widgets/remote_touch_view.dart';
@@ -48,6 +48,11 @@ class _ControllerHomeState extends State<ControllerHome> {
);
final _deviceIdController = TextEditingController();
final _targetController = TextEditingController(text: '981964879');
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
/// 账号 API 客户端(登录 / 刷新 / 绑定列表 / TURN
final ApiClient _apiClient = ApiClient();
RemoteController? _controller;
RTCVideoRenderer? _renderer;
@@ -56,6 +61,9 @@ class _ControllerHomeState extends State<ControllerHome> {
/// 是否正在连接信令服务器 / 建立 WebRTC连接过程中禁用“连接”按钮
bool _connecting = false;
String _status = '状态: 已停止';
/// 是否已登录accessToken 存在)。
bool _loggedIn = false;
String _stats = '';
double _videoAspect = 16 / 9;
@@ -95,15 +103,14 @@ class _ControllerHomeState extends State<ControllerHome> {
@override
void initState() {
super.initState();
_initDeviceId();
}
/// 异步获取设备标识并填入设备ID输入框非系统签名使用兜底方案
Future<void> _initDeviceId() async {
final id = await DeviceUtils.getSerialNumber();
if (mounted) {
setState(() => _deviceIdController.text = id);
}
// 启动时从安全存储恢复令牌设备ID 由服务端注册后下发,无需本地生成。
_apiClient.restore().then((_) async {
if (_apiClient.accessToken != null && await _apiClient.hasRefreshToken()) {
setState(() {
_loggedIn = true;
});
}
});
}
@override
@@ -137,12 +144,14 @@ class _ControllerHomeState extends State<ControllerHome> {
Future<void> _showAuthDialog() async {
final serverUrl = _serverUrlController.text.trim();
final deviceId = _deviceIdController.text.trim();
final target = _targetController.text.trim();
if (serverUrl.isEmpty || deviceId.isEmpty || target.isEmpty) {
_setStatus('请填写所有字段');
if (serverUrl.isEmpty || target.isEmpty) {
_setStatus('请填写服务器地址和目标设备ID');
return;
}
// 连接前确保已登录Bearer token
final authed = await _ensureLoggedIn();
if (!authed) return;
String selectedType = 'NONE';
final valueController = TextEditingController();
@@ -279,11 +288,10 @@ class _ControllerHomeState extends State<ControllerHome> {
Future<void> _connect({required String authType, required String authValue}) async {
final serverUrl = _serverUrlController.text.trim();
final deviceId = _deviceIdController.text.trim();
final target = _targetController.text.trim();
if (serverUrl.isEmpty || deviceId.isEmpty || target.isEmpty) {
_setStatus('请填写所有字段');
if (serverUrl.isEmpty || target.isEmpty) {
_setStatus('请填写服务器地址和目标设备ID');
return;
}
@@ -292,8 +300,9 @@ class _ControllerHomeState extends State<ControllerHome> {
_controller = RemoteController(
serverUrl: serverUrl,
deviceId: deviceId,
targetDeviceId: target,
apiClient: _apiClient,
token: _apiClient.accessToken,
authType: authType,
authValue: authValue,
);
@@ -378,10 +387,99 @@ class _ControllerHomeState extends State<ControllerHome> {
if (mounted) setState(() => _selfCodecSupported = false);
_showAlert('当前平台不支持自编码硬解,已回退到 WebRTC 媒体流。');
};
_controller!.onTokenExpired = () {
_showAlert('登录已失效,请重新登录后再连接。');
_resetLogin();
};
_controller!.onForceLogout = () {
_showAlert('账号已在其他位置登录,已强制下线。');
_resetLogin();
};
_controller!.connect(authType: authType, authValue: authValue);
}
/// 确保已登录:若已有 refreshToken 则直接返回;否则弹出登录对话框,登录成功后返回。 */
Future<bool> _ensureLoggedIn() async {
if (_apiClient.accessToken != null && await _apiClient.hasRefreshToken()) {
return true;
}
final login = await _showLoginDialog();
return login;
}
/// 显示登录对话框:输入用户名/密码,调用 /api/auth/login 保存令牌。
Future<bool> _showLoginDialog() async {
_usernameController.clear();
_passwordController.clear();
final result = await showCupertinoDialog<bool>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('登录'),
content: Padding(
padding: const EdgeInsets.only(top: 12),
child: Column(
children: [
CupertinoTextField(
controller: _usernameController,
placeholder: '用户名',
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
),
const SizedBox(height: 10),
CupertinoTextField(
controller: _passwordController,
placeholder: '密码',
obscureText: true,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
),
],
),
),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
onPressed: () => Navigator.of(ctx).pop(false),
),
CupertinoDialogAction(
child: const Text('登录'),
onPressed: () async {
final user = _usernameController.text.trim();
final pass = _passwordController.text.trim();
if (user.isEmpty || pass.isEmpty) {
_showAlert('请输入用户名和密码');
return;
}
try {
await _apiClient.login(user, pass);
if (mounted) {
setState(() {
_loggedIn = true;
});
}
Navigator.of(ctx).pop(true);
} catch (e) {
_showAlert('登录失败:$e');
}
},
),
],
),
);
return result ?? false;
}
/// 退出登录并复位 UI 状态(清空令牌、停止连接)。
Future<void> _resetLogin() async {
await _apiClient.clear();
await _disconnect();
if (mounted) {
setState(() {
_loggedIn = false;
_deviceIdController.clear();
});
}
}
void _onRendererUpdate() {
final w = _renderer?.value.width ?? 0;
final h = _renderer?.value.height ?? 0;
@@ -636,11 +734,12 @@ class _ControllerHomeState extends State<ControllerHome> {
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
const SizedBox(height: 16),
const Text('本机设备ID:', style: TextStyle(fontSize: 14)),
const Text('本机设备ID(连接后由服务端下发,无需填写):', style: TextStyle(fontSize: 14)),
const SizedBox(height: 8),
CupertinoTextField(
controller: _deviceIdController,
placeholder: '设备ID',
placeholder: '连接后由服务端下发',
enabled: false,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
const SizedBox(height: 16),
@@ -656,6 +755,21 @@ class _ControllerHomeState extends State<ControllerHome> {
_status,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: CupertinoButton.filled(
onPressed: _loggedIn
? () async {
await _resetLogin();
}
: () async {
final ok = await _showLoginDialog();
if (!ok) _setStatus('请先登录后再连接');
},
child: Text(_loggedIn ? '退出登录' : '登录账号'),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,