如何为前后端分离架构下的Node.js REST API公开GET接口做安全防护?
嘿,结合你的技术栈和需求,咱们来梳理下如何保护那些无需登录但不想被第三方爬取的GET API端点。首先明确:Helmet主要是帮你设置安全HTTP头(比如防XSS、点击劫持),加固基础安全,但它没法直接防止API被爬取,得搭配其他手段才行。下面是从易到难的最优方案组合:
1. 先做域名白名单(CORS + 反向代理验证)
这是最基础的一层,能挡掉浏览器端的非法跨域请求,以及大部分小白爬虫。
配置CORS限制
用Express的cors包,只允许你的前端域名https://www.example.com发起请求:
const cors = require('cors'); app.use(cors({ origin: 'https://www.example.com', credentials: true // 如果需要携带cookie(比如后续登录态),开启这个 }));
补充反向代理验证(防绕过CORS)
CORS是浏览器端的限制,爬虫可以用curl等工具直接绕过,所以要在后端加一层请求头校验:
// 先告诉Express信任Heroku的代理,这样能拿到真实的客户端请求头 app.set('trust proxy', true); // 自定义中间件校验Origin/Referer app.use((req, res, next) => { const allowedOrigin = 'https://www.example.com'; // 优先取Origin,没有的话取Referer const requestSource = req.get('Origin') || req.get('Referer'); // 校验请求来源是否合法 if (!requestSource || !requestSource.startsWith(allowedOrigin)) { return res.status(403).json({ error: 'Forbidden: Invalid request source' }); } next(); });
2. 加上请求频率限制(防暴力爬取)
即使是合法用户,也不能让爬虫无限次请求API。用express-rate-limit包设置合理的阈值:
const rateLimit = require('express-rate-limit'); const apiLimiter = rateLimit({ windowMs: 1 * 60 * 1000, // 时间窗口:1分钟 max: 100, // 每个IP最多100次请求 message: { error: '请求过于频繁,请稍后再试' }, standardHeaders: true, legacyHeaders: false }); // 给所有API端点应用限制 app.use('/api/', apiLimiter);
注意:因为Heroku是代理环境,必须先设置
app.set('trust proxy', true),否则rate-limit会把Heroku的代理IP当成客户端IP,导致所有请求共享同一个限制。
3. 签名请求(最高级的防爬方案)
如果需要更强的防护,这种方法能确保只有你的前端能发起有效请求,即使爬虫拿到了API地址,也没法构造合法请求。
实现思路
- 后端和前端(Next.js服务器端)共享一个密钥(存在环境变量里,不要硬编码);
- 前端每次请求API前,先调用Next.js的内部API路由生成签名;
- 后端验证签名的有效性,同时校验请求的时效性(防止重放攻击)。
代码示例
后端验证中间件
const crypto = require('crypto'); app.use((req, res, next) => { const { timestamp, signature } = req.query; const secret = process.env.API_SECRET; // 存在Heroku环境变量里 // 校验必要参数 if (!timestamp || !signature) { return res.status(403).json({ error: '缺少签名信息' }); } // 校验签名是否过期(比如5分钟内有效) if (Date.now() - parseInt(timestamp) > 5 * 60 * 1000) { return res.status(403).json({ error: '签名已过期' }); } // 生成预期签名并对比 const expectedSignature = crypto.createHmac('sha256', secret) .update(`${req.method}${req.path}${timestamp}`) .digest('hex'); if (signature !== expectedSignature) { return res.status(403).json({ error: '签名无效' }); } next(); });
Next.js内部API路由(生成签名)
在pages/api/get-signature.js里:
import crypto from 'crypto'; export default function handler(req, res) { const { path, method } = req.query; const timestamp = Date.now().toString(); const secret = process.env.API_SECRET; // 存在Vercel环境变量里 const signature = crypto.createHmac('sha256', secret) .update(`${method}${path}${timestamp}`) .digest('hex'); res.status(200).json({ timestamp, signature }); }
前端请求逻辑
const fetchPublicEvents = async () => { // 先从Next.js内部API获取签名 const signatureRes = await fetch('/api/get-signature?path=/api/events&method=GET'); const { timestamp, signature } = await signatureRes.json(); // 带着签名请求后端API const apiRes = await fetch(`https://api.example.com/api/events?timestamp=${timestamp}&signature=${signature}`); return apiRes.json(); };
4. 辅助手段:User-Agent过滤
可以过滤掉明显的爬虫标识,比如curl、Scrapy等,虽然容易被绕过,但能挡掉一部分低级爬虫:
app.use((req, res, next) => { const blockedAgents = ['curl', 'Python-urllib', 'Scrapy', 'PostmanRuntime']; const userAgent = req.get('User-Agent') || ''; if (blockedAgents.some(agent => userAgent.includes(agent))) { return res.status(403).json({ error: 'Forbidden' }); } next(); });
5. 别忘了用Helmet加固基础安全
虽然Helmet不防爬,但它能帮你设置关键安全头,比如:
const helmet = require('helmet'); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], connectSrc: ["'self'", "https://api.example.com"] // 限制前端只能从你的API拉数据 } }, xFrameOptions: 'DENY', // 防止点击劫持 xXssProtection: true // 启用XSS防护 }));
方案优先级建议
- 先配置CORS+反向代理验证,快速挡掉大部分非法请求;
- 加上请求频率限制,防止暴力爬取;
- 如果需要高安全性,再实现签名请求;
- User-Agent过滤作为辅助;
- Helmet一定要配置,加固基础安全。
注意:测试时要确保Next.js的SSR/SSG请求能正常通过(因为这类请求是从Vercel服务器发起的,要确保Origin/Referer能被正确识别)。
内容的提问来源于stack exchange,提问作者Christoph Berger

