Files
XCDesktop/remote/docs/开发/安全指南.md
2026-03-08 01:34:54 +08:00

15 KiB
Raw Blame History

安全指南

本文档详细说明了 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)
  • PayloaduserIdiat(签发时间)、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';
}

配置优先级

  1. JWT_SECRET - 推荐使用
  2. REMOTE_SECURITY_PASSWORD - 备用选项
  3. 默认密钥 - 仅开发环境,生产环境必须配置

推荐配置

# .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;
}

提取顺序

  1. Authorization HeaderAuthorization: Bearer <token>
  2. Cookietokenauth 字段
  3. 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 个月

更换步骤

  1. 生成新密钥:
openssl rand -base64 32
  1. 更新环境变量
  2. 重启服务(会使所有现有 Token 失效)
  3. 通知用户重新登录

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 密码泄露

风险来源

  • 配置文件被提交到版本控制
  • 日志中记录敏感信息
  • 服务器被入侵

防护措施

  1. 使用环境变量
# .env 文件(添加到 .gitignore
REMOTE_SECURITY_PASSWORD=$2b$12$...
JWT_SECRET=your-secret-key
  1. 检查日志输出
// 错误示例
logger.info('User login', { password: password });

// 正确示例
logger.info('User login', { userId: userId });
  1. 文件权限控制
chmod 600 .env
chown app:app .env

5.2 中间人攻击

攻击场景

  • HTTP 明文传输被监听
  • 公共 WiFi 环境被劫持
  • DNS 污染

防护措施

  1. 强制 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();
});
  1. 设置安全响应头
app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});
  1. 使用 VPN:在公共网络环境访问敏感服务

5.3 XSS 攻击

攻击场景

  • 用户输入未转义直接渲染
  • 恶意脚本窃取 Token
  • Cookie 被劫持

防护措施

  1. 设置 Cookie 安全属性
res.cookie('token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 24 * 60 * 60 * 1000
});
  1. 内容安全策略CSP
res.setHeader(
  'Content-Security-Policy',
  "default-src 'self'; script-src 'self'"
);
  1. 输入验证和转义
const escapeHtml = (str) => {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
};

5.4 CSRF 攻击

攻击场景

  • 恶意网站发送伪造请求
  • 利用用户已认证状态
  • 执行未授权操作

防护措施

  1. SameSite Cookie
res.cookie('token', token, {
  sameSite: 'strict'
});
  1. 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) => {
  // 处理请求
});
  1. 验证 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 认证(如适用)
  • 定期更换密钥和密码

相关文件