最近一个月,我翻遍了GitHub上12个使用Node.js的开源项目,从那些标着2k星标的起步模板,到正经公司在用的生产级脚手架,就为了看看大家怎么处理JWT(JSON Web Token)。结果让我有点坐不住:几乎每个项目里都藏着同样的6个错误。不是开发者不行,恰恰相反,写这些代码的人都很老练。问题在于那些从教程里复制粘贴过来的写法,被无数次照搬却没人停下来问一句——它真的安全吗?下面就是这6个坑,先上第一个,也是离谱到让你想发弹幕的那种。

第一个错误:密钥就叫“secret”,真的就是那个单词。

我在三个不同的项目里看到了下面这句:
const token = jwt.sign({ userId: user.id }, "secret", { expiresIn: "1h" });
这个“secret”几乎出现在互联网上每一个JWT教程里,JSON Web Token官网文档里有它,jsonwebtoken库的README里也有它。开发者写示例时随手一敲,后来忘了改,就直接跑在了线上。一个只有6个字符的ASCII密钥,大概能提供42比特的熵值。用GPU集群跑字典攻击,几毫秒就能把它撞开。随手生成一个真正安全的密钥只要3秒钟,用256位密码学随机数就好。信我,这3秒真不能省。

第二个错误:在鉴权中间件里用了 jwt.decode()。

我在一个生产环境的认证中间件里看到了这样的代码:
const decoded = jwt.decode(req.headers.authorization.split(" ")[1]);
if (!decoded.userId) return res.status(401).send("Unauthorized");
jwt.decode()根本不检查签名。它只管把payload原封不动地读出来,不管这个token是过期了、是伪造的、还是被人改写过。这意味着攻击者可以随手捏一个payload,只要能糊弄过这个userId检查,就能直接绕过认证。改法也很简单,就两个字:把decode换成verify。
const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ["HS256"] });
多两个字母,少一个天大漏洞。

第三个错误:verify()的时候没指定算法。

很多项目在验证token时是这么写的:
jwt.verify(token, secret);
没有第二个参数里的{algorithms:['HS256']},这个库就会老老实实地信任token头部里自己声明的算法。攻击者可以造一个token,头部写成"alg":"none",签名留空,verify()一看,哦你没说要验什么算法,那我信了,于是放行。记住:验证的时候永远显式地把预期的算法写死。别把选择权留给攻击者。

第四个错误:密钥直接提交到了版本控制里。

我在一个公开仓库的config.js里看到了这段:
module.exports = {
jwtSecret: "productionsecretdonotshare2024",
database: process.env.DATABASE_URL
};
注意那个注释——“donotshare”。开发者自己也知道这是敏感信息,可它就是被commit进了公开仓库。一旦进了git历史,这个密钥就永久泄露了。哪怕你之后删掉文件,历史记录里照样扒得出来。如果你也这么干过,别犹豫,马上轮换密钥,别抱幻想。

第五个错误:把token存在localStorage里。

我在好几个项目的前端代码中都发现了同一套操作:
localStorage.setItem("token", response.data.token);
// 然后要用的时候:
const token = localStorage.getItem("token");
localStorage里的内容对页面上跑的任何JavaScript都是透明的。只要有一个XSS漏洞——比如一条没过滤干净的评论、一个被污染的npm包、一个第三方脚本——所有存着的token就会被人直接卷走。最好的做法是用httpOnly的cookie来放鉴权token。它对JavaScript不可见,XSS再怎么折腾也偷不走。如果需要防CSRF,再配上SameSite属性就好。

第六个错误:token永远不过期。

有的项目签发token的时候压根没设expiresIn:
const token = jwt.sign({ userId: user.id }, secret);
这意味着一旦生成,这个token永生不死。用户可能早就注销了,设备可能早就丢了,但只要token本身还在,谁拿到它都能一直用下去。永远给token一个合理的过期时间,同时准备好刷新令牌的机制。即便只是设成24小时,也比永远要好上无数倍。

这6个错误都没什么高深的技术门槛,它们只是被复制粘贴得太顺手,顺手到整个社区都快忘了原本不该这么写。把这些小零件换好,你的JWT实现就不会再是下一个被轻松穿过的筛子。