15 KiB
15 KiB
安全指南
本文档详细说明了 Remote Control 项目的安全机制和最佳实践。
目录
1. 密码安全
1.1 bcrypt 哈希算法
本项目使用 bcrypt 算法对密码进行哈希处理。bcrypt 是一种专门为密码存储设计的哈希算法,具有以下优势:
- 自带盐值:每次哈希自动生成随机盐值,防止彩虹表攻击
- 计算成本可调:通过 cost factor 控制计算复杂度,抵御暴力破解
- 抗 GPU/ASIC 攻击:内存密集型设计,不适合硬件加速
const bcrypt = require('bcrypt');
const BCRYPT_COST = 12;
async function hashPassword(password) {
return bcrypt.hash(password, BCRYPT_COST);
}
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
Cost Factor 说明:
- 项目使用
BCRYPT_COST = 12(约 4096 轮迭代) - Cost 值每增加 1,计算时间翻倍
- 推荐范围:10-12(生产环境),可根据服务器性能调整
1.2 密码迁移脚本使用
项目提供了密码迁移脚本 scripts/migrate-password.js,用于将明文密码转换为 bcrypt 哈希值。
使用方法:
node scripts/migrate-password.js
交互示例:
=== 密码迁移脚本 ===
此脚本将明文密码转换为 bcrypt 哈希值
请输入要哈希的密码 (或输入 q 退出): MySecurePassword123
--- 结果 ---
明文密码: MySecurePassword123
bcrypt 哈希: $2b$10$abcdefghijklmnopqrstuvwxABCDEFGHIJ
请将哈希值更新到环境变量或配置文件中
配置方式:
将生成的哈希值配置到环境变量:
# .env 文件
REMOTE_SECURITY_PASSWORD=$2b$10$abcdefghijklmnopqrstuvwxABCDEFGHIJ
1.3 明文密码 vs 哈希密码
系统支持两种密码配置方式,但强烈推荐使用哈希密码:
| 特性 | 明文密码 | 哈希密码 |
|---|---|---|
| 配置方式 | REMOTE_SECURITY_PASSWORD=mypassword |
REMOTE_SECURITY_PASSWORD=$2b$12$... |
| 识别方式 | 不以 $2b$ 开头 |
以 $2b$ 开头 |
| 安全性 | 低 | 高 |
| 泄露风险 | 配置文件泄露即密码泄露 | 哈希值泄露无法反推原密码 |
密码验证流程:
async authenticate(password) {
if (this.isHashed) {
return await this.verifyPassword(password, this.passwordHash);
} else {
return password === this.passwordHash;
}
}
⚠️ 警告:明文密码仅用于开发测试环境,生产环境必须使用 bcrypt 哈希密码。
2. JWT Token 管理
2.1 Token 生成
Token 使用 jsonwebtoken 库生成,包含用户标识和签发时间:
const TOKEN_EXPIRY = '24h';
generateToken(payload) {
const tokenPayload = {
userId: payload.userId || 'default-user',
iat: Math.floor(Date.now() / 1000)
};
const options = {
expiresIn: TOKEN_EXPIRY
};
return jwt.sign(tokenPayload, this.secret, options);
}
Token 结构:
- Header:算法类型 (HS256)
- Payload:
userId、iat(签发时间)、exp(过期时间) - Signature:使用密钥签名
2.2 Token 验证
verifyToken(token) {
if (!token) {
return null;
}
try {
const decoded = jwt.verify(token, this.secret);
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.warn('Token expired', { expiredAt: error.expiredAt });
} else if (error.name === 'JsonWebTokenError') {
console.warn('Invalid token', { error: error.message });
}
return null;
}
}
验证结果:
- 成功:返回解码后的 payload 对象
- Token 过期:返回
null,记录过期时间 - Token 无效:返回
null,记录错误信息
2.3 Token 有效期
- 默认有效期:24 小时
- 续期策略:Token 过期后需重新认证
- 建议:根据业务需求调整有效期
const TOKEN_EXPIRY = '24h';
const options = {
expiresIn: TOKEN_EXPIRY
};
2.4 密钥配置
密钥优先级顺序:
_getSecret() {
if (process.env.JWT_SECRET) {
return process.env.JWT_SECRET;
}
if (process.env.REMOTE_SECURITY_PASSWORD) {
return process.env.REMOTE_SECURITY_PASSWORD;
}
return 'remote-control-default-secret-change-in-production';
}
配置优先级:
JWT_SECRET- 推荐使用REMOTE_SECURITY_PASSWORD- 备用选项- 默认密钥 - 仅开发环境,生产环境必须配置
推荐配置:
# .env 文件
JWT_SECRET=your-very-long-random-secret-key-at-least-32-characters
REMOTE_SECURITY_PASSWORD=$2b$12$your-bcrypt-hashed-password
⚠️ 警告:生产环境必须设置
JWT_SECRET,使用默认密钥将导致严重安全风险。
3. 认证中间件
3.1 Token 提取方式
认证中间件支持多种 Token 提取方式:
function extractToken(req) {
if (req.headers.authorization) {
const parts = req.headers.authorization.split(' ');
if (parts.length === 2 && parts[0] === 'Bearer') {
return parts[1];
}
}
if (req.cookies) {
if (req.cookies.token) {
return req.cookies.token;
}
if (req.cookies.auth) {
return req.cookies.auth;
}
}
if (req.query && req.query.token) {
return req.query.token;
}
return null;
}
提取顺序:
- Authorization Header:
Authorization: Bearer <token> - Cookie:
token或auth字段 - Query Parameter:
?token=<token>
3.2 密码验证
当 Token 无效或不存在时,支持通过密码进行认证:
const password = req.query.password || req.body?.password;
if (password) {
const isValid = await authService.authenticate(password);
if (isValid) {
req.user = { userId: 'default-user' };
res.locals.authenticated = true;
return next();
}
}
密码传递方式:
- Query Parameter:
?password=<password> - Request Body:
{ "password": "<password>" }
3.3 认证流程
┌─────────────────────────────────────────────────────────────┐
│ 认证中间件流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 检查是否配置密码 │
│ └─ 无密码配置 → 跳过认证,允许访问 │
│ │
│ 2. 提取 Token │
│ ├─ Authorization Header │
│ ├─ Cookie │
│ └─ Query Parameter │
│ │
│ 3. 验证 Token │
│ ├─ 有效 → 设置用户信息,允许访问 │
│ └─ 无效/过期 → 继续下一步 │
│ │
│ 4. 提取密码 │
│ ├─ Query Parameter │
│ └─ Request Body │
│ │
│ 5. 验证密码 │
│ ├─ 正确 → 允许访问 │
│ └─ 错误 → 返回 401 │
│ │
└─────────────────────────────────────────────────────────────┘
完整代码:
async function authMiddleware(req, res, next) {
const authService = AuthService.getInstance();
const tokenManager = TokenManager.getInstance();
if (!authService.hasPassword()) {
req.user = { userId: 'default-user' };
res.locals.authenticated = true;
return next();
}
const token = extractToken(req);
if (token) {
const decoded = tokenManager.verifyToken(token);
if (decoded) {
req.user = { userId: decoded.userId };
res.locals.authenticated = true;
return next();
}
}
const password = req.query.password || req.body?.password;
if (password) {
const isValid = await authService.authenticate(password);
if (isValid) {
req.user = { userId: 'default-user' };
res.locals.authenticated = true;
return next();
}
}
res.status(401).json({
error: 'Authentication required',
code: 'AUTH_REQUIRED'
});
}
4. 安全建议
4.1 使用强密码
- 长度:至少 12 个字符
- 复杂度:包含大小写字母、数字、特殊字符
- 避免:常见单词、生日、连续字符
# 生成强密码示例
openssl rand -base64 16
4.2 启用 HTTPS
为什么需要 HTTPS:
- 加密传输数据,防止中间人攻击
- 保护 Token 和密码不被窃听
- 防止内容被篡改
配置示例(Nginx):
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
}
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
4.3 定期更换密钥
建议更换周期:
- JWT_SECRET:每 3-6 个月
- 密码:每 3 个月
更换步骤:
- 生成新密钥:
openssl rand -base64 32
- 更新环境变量
- 重启服务(会使所有现有 Token 失效)
- 通知用户重新登录
4.4 限制访问 IP
Nginx 配置示例:
location / {
allow 192.168.1.0/24;
allow 10.0.0.0/8;
deny all;
proxy_pass http://localhost:3000;
}
应用层限制:
const ALLOWED_IPS = ['192.168.1.0/24', '10.0.0.0/8'];
function ipFilter(req, res, next) {
const clientIp = req.ip || req.connection.remoteAddress;
if (!isIpAllowed(clientIp, ALLOWED_IPS)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
}
4.5 使用 FRP Token
如果通过 FRP 进行内网穿透,建议配置 FRP Token:
frps.ini(服务端):
[common]
bind_port = 7000
authentication_method = token
token = your-secure-frp-token
frpc.ini(客户端):
[common]
server_addr = your-server.com
server_port = 7000
authentication_method = token
token = your-secure-frp-token
[web]
type = http
local_port = 3000
custom_domains = your-domain.com
5. 常见安全风险
5.1 密码泄露
风险来源:
- 配置文件被提交到版本控制
- 日志中记录敏感信息
- 服务器被入侵
防护措施:
- 使用环境变量:
# .env 文件(添加到 .gitignore)
REMOTE_SECURITY_PASSWORD=$2b$12$...
JWT_SECRET=your-secret-key
- 检查日志输出:
// 错误示例
logger.info('User login', { password: password });
// 正确示例
logger.info('User login', { userId: userId });
- 文件权限控制:
chmod 600 .env
chown app:app .env
5.2 中间人攻击
攻击场景:
- HTTP 明文传输被监听
- 公共 WiFi 环境被劫持
- DNS 污染
防护措施:
- 强制 HTTPS:
app.use((req, res, next) => {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.get('host')}${req.url}`);
}
next();
});
- 设置安全响应头:
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
- 使用 VPN:在公共网络环境访问敏感服务
5.3 XSS 攻击
攻击场景:
- 用户输入未转义直接渲染
- 恶意脚本窃取 Token
- Cookie 被劫持
防护措施:
- 设置 Cookie 安全属性:
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000
});
- 内容安全策略(CSP):
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'"
);
- 输入验证和转义:
const escapeHtml = (str) => {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
5.4 CSRF 攻击
攻击场景:
- 恶意网站发送伪造请求
- 利用用户已认证状态
- 执行未授权操作
防护措施:
- SameSite Cookie:
res.cookie('token', token, {
sameSite: 'strict'
});
- CSRF Token:
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// 处理请求
});
- 验证 Referer 头:
app.use((req, res, next) => {
const referer = req.get('referer');
const allowedOrigins = ['https://your-domain.com'];
if (referer && !allowedOrigins.some(origin => referer.startsWith(origin))) {
return res.status(403).json({ error: 'Invalid referer' });
}
next();
});
安全检查清单
在部署到生产环境前,请确保完成以下检查:
- 使用 bcrypt 哈希密码(非明文)
- 配置强随机 JWT_SECRET
- 启用 HTTPS
- 设置 Cookie 安全属性(httpOnly, secure, sameSite)
- 配置安全响应头(HSTS, CSP)
- 敏感文件未提交到版本控制
- 日志不包含敏感信息
- 配置 IP 访问限制(如适用)
- FRP 配置 Token 认证(如适用)
- 定期更换密钥和密码
相关文件
- AuthService.js - 认证服务实现
- TokenManager.js - Token 管理器
- auth.js - 认证中间件
- migrate-password.js - 密码迁移脚本