feat(remote): 完善远程桌面认证机制

1. 修复 WebSocket 认证漏洞:WebSocket 连接现在需要认证(支持 URL 参数 password 或 Cookie token)

2. 支持 URL 参数自动登录:HTTP 请求带 ?password=xxx 参数时会自动验证并设置 cookie

3. 主程序添加密码配置:
   - RemoteDevice 类型添加 password 字段
   - ConfigDialog 添加密码输入框
   - 打开远程桌面时传递 password 参数

4. 修复 remote/public/js/app.js:
   - 从 URL 参数获取 password 并传递给 WebSocket 连接
   - 移除错误的 token 当作 password 的代码

5. 添加密码变化检测:修改密码后自动刷新页面重新认证,无需重启 remote 服务

6. 文件传输 API 支持 password 参数
This commit is contained in:
2026-03-09 00:54:48 +08:00
parent 8531d916a3
commit 50cfc8835f
10 changed files with 187 additions and 32 deletions

View File

@@ -1,9 +1,9 @@
(function() {
const password = getCookie('auth') || '';
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsHost = window.location.hostname;
const wsPort = window.location.port;
const urlParams = new URLSearchParams(window.location.search);
const password = urlParams.get('password');
const wsUrlBase = wsPort ? `${wsProtocol}//${wsHost}:${wsPort}/ws` : `${wsProtocol}//${wsHost}/ws`;
const WS_URL = password ? `${wsUrlBase}?password=${encodeURIComponent(password)}` : wsUrlBase;

View File

@@ -240,7 +240,7 @@ class App {
httpServer.renderLoginPage(res, '密码错误');
});
httpServer.use((req, res, next) => {
httpServer.use(async (req, res, next) => {
if (!authService.hasPassword()) {
res.locals.authenticated = true;
return next();
@@ -250,8 +250,31 @@ class App {
if (token) {
const decoded = tokenManager.verifyToken(token);
if (decoded) {
logger.info('HTTP auth: token valid from cookie', { path: req.path, ip: req.socket?.remoteAddress });
res.locals.authenticated = true;
return next();
} else {
logger.info('HTTP auth: token invalid from cookie', { path: req.path, ip: req.socket?.remoteAddress });
}
}
// 检查 URL 参数中的 password
const urlPassword = req.query.password;
if (urlPassword) {
logger.info('HTTP auth: checking password from URL', {
path: req.path,
ip: req.socket?.remoteAddress,
passwordLength: urlPassword.length
});
const isValid = await authService.authenticate(urlPassword);
if (isValid) {
const newToken = tokenManager.generateToken({ userId: 'default-user' });
res.cookie('auth', newToken, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 });
logger.info('HTTP auth: password valid, token generated', { path: req.path, ip: req.socket?.remoteAddress });
res.locals.authenticated = true;
return next();
} else {
logger.warn('HTTP auth: password invalid', { path: req.path, ip: req.socket?.remoteAddress });
}
}
@@ -283,18 +306,63 @@ class App {
const originalHandlers = this.wss.listeners('connection');
this.wss.removeAllListeners('connection');
const securityConfig = require('../utils/config').getSecurityConfig();
const password = securityConfig.password;
// 未认证的连接也允许,用于剪贴板同步
this.wss.on('connection', (ws, req) => {
this.wss.on('connection', async (ws, req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const isAuthenticated = url.searchParams.get('password') === password;
const fullUrl = req.url;
let isAuthenticated = false;
let authMethod = '';
// 检查 URL 参数中的 password
const urlPassword = url.searchParams.get('password');
if (urlPassword) {
logger.info('WebSocket auth: checking password from URL', {
ip: req.socket?.remoteAddress,
urlLength: fullUrl.length,
passwordLength: urlPassword.length,
passwordPrefix: urlPassword.substring(0, 2) + '***'
});
const isValid = await authService.authenticate(urlPassword);
if (isValid) {
isAuthenticated = true;
authMethod = 'password_url';
logger.info('WebSocket auth: password from URL valid', { ip: req.socket?.remoteAddress });
} else {
logger.warn('WebSocket auth: password from URL invalid', { ip: req.socket?.remoteAddress });
}
}
// 检查 Cookie 中的 token
if (!isAuthenticated && req.cookies && req.cookies.auth) {
logger.info('WebSocket auth: checking token from cookie', { ip: req.socket?.remoteAddress });
const decoded = tokenManager.verifyToken(req.cookies.auth);
if (decoded) {
isAuthenticated = true;
authMethod = 'cookie_token';
logger.info('WebSocket auth: token from cookie valid', { ip: req.socket?.remoteAddress });
} else {
logger.warn('WebSocket auth: token from cookie invalid', { ip: req.socket?.remoteAddress });
}
}
// 未认证,拒绝连接
if (!isAuthenticated) {
logger.warn('WebSocket authentication failed', {
ip: req.socket?.remoteAddress,
hasPassword: !!urlPassword,
hasCookie: !!(req.cookies && req.cookies.auth),
fullUrl: fullUrl.substring(0, 200)
});
ws.close(1008, 'Authentication required');
return;
}
logger.debug('WebSocket authenticated', { ip: req.socket?.remoteAddress, authMethod });
// 保存认证状态
ws.isAuthenticated = isAuthenticated;
ws.isAuthenticated = true;
// 处理输入消息(不检查认证)
// 处理输入消息
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
@@ -304,7 +372,7 @@ class App {
}
});
// 调用原始 handlers(用于已认证的连接)
// 调用原始 handlers
originalHandlers.forEach(handler => {
handler(ws, req);
});