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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user