feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑 - WebSocket改用Bearer令牌认证,移除REGISTER请求 - 设备ID改为服务端下发,支持令牌刷新和强制下线处理 - 新增deviceSecret加密存储和accessToken自动刷新 - 更新设备ID获取方式为出厂SN,添加安全存储依赖
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user