HTTPS与安全

在当今的网络环境中,安全性是任何 Web 应用的基础。HTTPS 通过加密通信保护数据传输,而安全实践则可以防御各种常见攻击。本章将详细介绍如何在 Node.js 应用中启用 HTTPS,并采取一系列安全措施保护你的应用和用户数据。

1. 为什么需要 HTTPS?

HTTPS(HTTP over SSL/TLS)在 HTTP 协议基础上增加了 SSL/TLS 加密层,提供以下保障:

  • 加密:防止数据在传输过程中被窃听。
  • 数据完整性:防止数据被篡改。
  • 身份验证:确保客户端与预期的服务器通信,防止中间人攻击。

现代浏览器会对未启用 HTTPS 的网站标记“不安全”,并且许多新特性(如 Service Workers、地理定位)仅支持 HTTPS。

2. 生成 SSL/TLS 证书

要启用 HTTPS,你需要一个 SSL 证书。证书通常从证书颁发机构(CA)购买,但开发环境可以使用自签名证书。

2.1 开发环境:自签名证书

使用 OpenSSL 工具生成自签名证书(需要安装 OpenSSL):

# 生成私钥
openssl genrsa -out key.pem 2048

# 生成证书签名请求(CSR)
openssl req -new -key key.pem -out csr.pem

# 生成自签名证书(有效期365天)
openssl x509 -req -days 365 -in csr.pem -signkey key.pem -out cert.pem

也可以一步生成:openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes

注意:自签名证书在浏览器中会显示不安全,需要手动信任,仅用于开发测试。

2.2 生产环境:获取 CA 证书

生产环境应从受信任的 CA 获取证书,例如:

  • Let's Encrypt:免费、自动化,推荐使用。
  • 商业 CA:如 DigiCert、GlobalSign、Comodo 等。

Let's Encrypt 的证书可以通过 certbot 工具自动获取和续期。详情见后文。

3. 在 Node.js 中创建 HTTPS 服务器

Node.js 内置了 https 模块,可以创建 HTTPS 服务器。你需要提供证书和私钥文件。

const https = require('https');
const fs = require('fs');
const path = require('path');

const options = {
  key: fs.readFileSync(path.join(__dirname, 'key.pem')),
  cert: fs.readFileSync(path.join(__dirname, 'cert.pem'))
};

const server = https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('Hello HTTPS!');
});

server.listen(443, () => {
  console.log('HTTPS server running on port 443');
});

注意:端口 443 是 HTTPS 默认端口,运行可能需要管理员权限。你也可以使用其他端口如 8443。

4. 在 Express 应用中启用 HTTPS

大多数应用使用 Express 框架。将 Express 应用作为请求处理函数传递给 HTTPS 服务器即可:

const express = require('express');
const https = require('https');
const fs = require('fs');

const app = express();

app.get('/', (req, res) => {
  res.send('Hello Express over HTTPS!');
});

const options = {
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
};

https.createServer(options, app).listen(443, () => {
  console.log('HTTPS server running on port 443');
});

你也可以同时启动 HTTP 服务器,用于重定向到 HTTPS(见下节)。

5. 强制 HTTPS 重定向

通常我们希望所有 HTTP 请求都重定向到 HTTPS。可以使用中间件实现:

app.use((req, res, next) => {
  if (!req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }
  next();
});

如果应用运行在代理(如 Nginx)后面,需要设置信任代理,因为 req.secure 可能检测不到。可以在 Express 中启用信任代理:

app.enable('trust proxy');

然后检查 req.protocolreq.headers['x-forwarded-proto']

6. 使用 Let's Encrypt 自动化证书

Let's Encrypt 提供免费证书,并通过 ACME 协议自动化续期。常用的客户端是 certbot

获取证书(以 Nginx 为例,但 Node.js 应用也可使用 standalone 插件):

sudo certbot certonly --standalone -d example.com

证书文件会生成在 /etc/letsencrypt/live/example.com/ 目录下,包含 privkey.pem(私钥)、fullchain.pem(证书链)等。然后在 Node.js 中读取这些文件:

const options = {
  key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem')
};

设置 crontab 定时任务自动续期(certbot 会处理):

0 0 * * * /usr/bin/certbot renew --quiet

注意:续期后可能需要重启 Node.js 应用加载新证书,可以通过 --post-hook 实现。

7. 安全最佳实践

除了 HTTPS,应用层面也需要实施多种安全措施。

7.1 使用 Helmet 中间件

Helmet 是一个 Express 中间件集合,通过设置各种 HTTP 头来增强安全性。它包含 15 个较小的中间件,如 helmet.hsts()helmet.frameguard() 等。

安装:npm install helmet

const express = require('express');
const helmet = require('helmet');
const app = express();

app.use(helmet()); // 使用默认配置

Helmet 默认会设置以下头部(部分):

  • Strict-Transport-Security(HSTS)
  • X-Frame-Options 防止点击劫持
  • X-Content-Type-Options 防止 MIME 嗅探
  • Referrer-Policy
  • Content-Security-Policy(需配置)

7.2 跨域资源共享(CORS)

如果你的 API 需要被其他域名的前端访问,需要正确配置 CORS。可以使用 cors 中间件。

安装:npm install cors

const cors = require('cors');
app.use(cors()); // 允许所有来源(生产环境应限制)

限制特定来源:

app.use(cors({
  origin: 'https://example.com'
}));

7.3 防止 SQL 注入

始终使用参数化查询或 ORM/ODM 来构建数据库查询。例如使用 MySQL 的预处理语句:

db.execute('SELECT * FROM users WHERE id = ?', [userId]);

避免拼接字符串:

// 危险!不要这样做
db.query(`SELECT * FROM users WHERE name = '${userInput}'`);

7.4 防止跨站脚本(XSS)

  • 对用户输入进行转义或清理。可以使用 escape-html 库。
  • 设置 Content-Security-Policy 头限制资源加载来源。
  • 使用 Helmet 的 xssFilter(已包含在默认配置中)。

7.5 速率限制(Rate Limiting)

防止暴力破解和 DoS 攻击。可以使用 express-rate-limit 中间件。

安装:npm install express-rate-limit

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100 // 每个IP限制100个请求
});

app.use('/api', limiter);

7.6 安全地存储密码

永远不要明文存储密码。使用 bcrypt 等算法哈希密码。

安装:npm install bcrypt

const bcrypt = require('bcrypt');
const saltRounds = 10;

// 哈希
const hash = await bcrypt.hash(password, saltRounds);

// 验证
const match = await bcrypt.compare(inputPassword, storedHash);

7.7 使用安全的 Session 配置

如果使用 session,确保配置安全:

app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production', // 仅HTTPS
    sameSite: 'strict'
  }
}));

7.8 限制请求体大小

防止超大请求体消耗资源:

app.use(express.json({ limit: '10kb' }));

8. 其他安全头设置

除了 Helmet,你还可以手动设置一些重要的头部:

  • Strict-Transport-Security (HSTS):强制浏览器只使用 HTTPS。
  • X-Frame-Options:防止点击劫持,可设为 DENYSAMEORIGIN
  • X-Content-Type-Options:设为 nosniff 防止 MIME 嗅探。
  • Content-Security-Policy (CSP):详细控制资源加载来源,有效防御 XSS。

Helmet 已经提供了这些,你也可以手动配置:

app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
  res.setHeader('X-Frame-Options', 'DENY');
  next();
});

9. 使用 Nginx 作为反向代理

生产环境通常将 Node.js 应用运行在内部端口,前面使用 Nginx 或 Apache 作为反向代理处理 SSL 和负载均衡。这样可以简化证书管理,并利用 Nginx 的高性能静态文件处理。

示例 Nginx 配置:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

此时 Node.js 应用不需要处理 SSL,但需要设置信任代理:app.enable('trust proxy');

10. 安全清单

  • [ ] 启用 HTTPS,使用有效证书。
  • [ ] 设置 HSTS 头,强制 HTTPS。
  • [ ] 使用 Helmet 设置安全头。
  • [ ] 配置 CORS 限制允许来源。
  • [ ] 输入验证和输出转义,防止 XSS。
  • [ ] 使用参数化查询,防止 SQL 注入。
  • [ ] 对密码进行哈希(bcrypt)。
  • [ ] 实施速率限制。
  • [ ] 限制请求体大小。
  • [ ] 使用安全的 session 配置(httpOnly、secure、sameSite)。
  • [ ] 定期更新依赖,修复已知漏洞。
  • [ ] 使用 npm audit 检查安全漏洞。
总结: 安全是一个持续的过程,而非一次性工作。通过 HTTPS 加密传输,配合 Helmet 等中间件和良好的编码习惯,可以显著提高 Node.js 应用的安全性。在生产环境中,建议使用 Nginx 作为反向代理处理 SSL,并定期检查依赖漏洞。