如何基于Node.js实现WebSocket请求来源域名的100%防伪造验证?
解决方案:基于域名控制权验证的WebSocket来源校验
要实现100%可靠的域名来源验证,核心是让客户端证明自己实际控制目标域名,而非依赖可伪造的请求头。以下是适配你的Node.js场景的方案,全程耗时可控制在1秒内:
核心流程(分两步)
1. 预验证:域名控制权证明
客户端发起WebSocket连接前,先完成域名归属校验:
- 客户端向服务器发HTTP POST请求,携带自身
id(或直接传域名)。 - 服务器通过
id从数据库查出对应域名targetDomain,生成10秒有效期的一次性随机挑战字符串(用crypto.randomBytes(32).toString('hex')生成),将挑战、域名、过期时间存入缓存(Redis/内存缓存均可,保证查询速度)。 - 服务器返回挑战给客户端,要求客户端在
https://${targetDomain}/.well-known/ws-challenge/${challengeId}路径下返回该挑战内容(客户端需提前部署静态文件服务,或动态生成该路径的响应)。 - 客户端告知服务器准备就绪后,服务器立即发起GET请求到上述客户端域名路径,验证返回内容与缓存的挑战是否一致。
- 验证通过后,服务器生成绑定该域名的临时JWT令牌(有效期设为30秒),令牌payload包含
domain和exp字段,返回给客户端。
2. WebSocket连接校验
客户端发起WebSocket连接时,在请求头Authorization字段携带临时令牌:
- 服务器在WebSocket握手的
upgrade事件中解析令牌,验证签名、有效期,以及令牌中的domain是否与数据库中id对应的域名匹配(若客户端传了id)。 - 验证通过则建立连接,否则直接拒绝。
Node.js代码示例
依赖安装
npm install express ws jsonwebtoken crypto redis axios
核心代码片段
预验证服务(Express)
const express = require('express'); const crypto = require('crypto'); const jwt = require('jsonwebtoken'); const redis = require('redis'); const axios = require('axios'); const app = express(); app.use(express.json()); const redisClient = redis.createClient(); redisClient.connect(); // 模拟从数据库获取域名的逻辑(替换为你的实际查询代码) async function getDomainById(id) { const domainMap = { 'client_001': 'demo-client-1.com', 'client_002': 'demo-client-2.com' }; return domainMap[id]; } // 生成并存储挑战 app.post('/ws/pre-verify', async (req, res) => { const { id } = req.body; const targetDomain = await getDomainById(id); if (!targetDomain) return res.status(404).send('无效ID'); const challengeId = crypto.randomBytes(16).toString('hex'); const challengeValue = crypto.randomBytes(32).toString('hex'); // 缓存挑战,10秒后自动过期 await redisClient.setEx(challengeId, 10, JSON.stringify({ domain: targetDomain, value: challengeValue })); res.json({ challengeId, challengeValue, verifyPath: `/.well-known/ws-challenge/${challengeId}` }); }); // 验证挑战并返回临时令牌 app.post('/ws/verify-challenge', async (req, res) => { const { challengeId } = req.body; const challengeData = await redisClient.get(challengeId); if (!challengeData) return res.status(400).send('挑战已过期或无效'); const { domain, value } = JSON.parse(challengeData); try { // 请求客户端域名验证挑战,超时设为500ms const response = await axios.get(`https://${domain}/.well-known/ws-challenge/${challengeId}`, { timeout: 500 }); if (response.data.trim() !== value) return res.status(403).send('挑战内容不匹配'); // 生成30秒有效期的临时令牌 const token = jwt.sign( { domain, exp: Math.floor(Date.now() / 1000) + 30 }, 'your-secure-jwt-secret' // 替换为你的密钥 ); res.json({ token }); } catch (err) { res.status(403).send('域名控制权验证失败'); } });
WebSocket服务(ws库)
const WebSocket = require('ws'); const jwt = require('jsonwebtoken'); const { createServer } = require('http'); const server = createServer(app); const wss = new WebSocket.Server({ server }); wss.on('connection', (ws, req) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { ws.close(401, '未授权'); return; } const token = authHeader.split(' ')[1]; try { const payload = jwt.verify(token, 'your-secure-jwt-secret'); // 可额外验证payload中的domain是否匹配客户端传入的id(若需要) console.log(`已验证连接来自域名:${payload.domain}`); // 后续业务逻辑 ws.on('message', (data) => { ws.send(`收到消息:${data}`); }); } catch (err) { ws.close(403, '无效令牌'); } }); server.listen(3000, () => { console.log('服务运行在端口3000'); });
方案可靠性说明
- 完全避免请求头伪造:攻击者必须实际控制目标域名,才能返回正确的挑战内容,从根源上解决伪造问题。
- 耗时可控:预验证的HTTP请求超时设为500ms,加上令牌生成和校验,总耗时远低于1秒。
- 风险可控:临时令牌有效期极短,即使泄露也无法被长期滥用。
优化建议
- 缓存优先用Redis或内存缓存(如
node-cache),比数据库查询更快,符合耗时要求。 - 给客户端的验证路径添加IP白名单,只允许你的服务器IP访问,避免他人恶意请求。
- 若客户端无法部署静态文件,可改用DNS TXT记录验证(服务器查询客户端域名的TXT记录是否包含挑战值),但需调整DNS查询超时时间(建议设为800ms内)。
内容的提问来源于stack exchange,提问作者Gura
相关产品推荐
相关产品推荐

