Node.js校验前端传入JWT报must be string/malformed错误
问题现象
React前端传递JWT到Node.js后端鉴权过程中先后抛出两类错误:
- 未做类型转换时,报错:
JsonWebTokenError: jwt must be a string - 手动对获取到的token调用
toString()转换后,报错变为:JsonWebTokenError: jwt malformed
排查确认前端传入的accessToken到达鉴权中间件时被识别为对象类型。
根因分析
核心错误共3处,按影响优先级排序:
- Express中间件参数顺序写反:Express中间件参数固定顺序为
(req, res, next),现有代码写为(res, req, next),导致实际调用res.header()尝试获取请求头——res是响应对象,本身不存在获取请求头的能力,拿到的是对象类型的内置属性/方法,这是触发错误的根本原因。对对象调用toString()会得到固定字符串[object Object],完全不符合JWT的三段式格式,自然触发格式错误。 - 登录接口返回格式不规范:直接将token字符串作为整个响应体返回,前端如果未做精准取值直接存储,很容易存入非字符串格式的内容。
- 请求头传递不规范:自定义请求头未遵循通用约定,也未使用JWT标准的
Authorization头传递方案,容易被网关、代理层篡改。
另外代码中存在拼写问题:accesToken漏写了s,应为accessToken,容易引发不必要的取值错误。
修复方案
1. 修正鉴权中间件AuthMiddleware.js
调整参数顺序,移除无效的toString转换,补充标准JWT头兼容逻辑、状态码返回和用户信息挂载逻辑:
const { verify } = require("jsonwebtoken") const validateToken = (req, res, next) => { // 优先取标准Authorization头,兼容旧的自定义头写法 const authHeader = req.header("Authorization") || req.header("accessToken"); if (!authHeader) { return res.status(401).json({ error: "user not logged in" }) } // 处理Bearer <token>的标准格式,截取有效token段 const accessToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader; try { const validToken = verify(accessToken, "importantSecret"); // 将解析后的用户信息挂载到req对象,供后续接口直接使用 req.user = validToken; next(); } catch (err) { console.log(err, "token校验失败") return res.status(403).json({ error: "invalid token" }) } } module.exports = { validateToken }
2. 修正登录接口User.js返回逻辑
将token包装为对象返回,补充对应错误状态码,增加token过期时间配置:
const express = require("express"); const router = express.Router() const { Users } = require("../models") const bcrypt = require("bcrypt") const { sign } = require("jsonwebtoken") router.post("/login", async (req, res) => { const { username, password } = req.body; const user = await Users.findOne({ where: { username: username } }); if (!user) { return res.status(404).json({ error: "User doesn't exist" }) } bcrypt.compare(password, user.password) .then((match) => { if (!match) { return res.status(401).json({ error: "Wrong Username or Password" }) } // 增加过期时间配置,避免token永久有效 const accessToken = sign( { username: user.username, id: user.id }, "importantSecret", { expiresIn: "1d" } ) // 包装为对象返回,方便前端精准取值 res.json({ accessToken }) }) }) module.exports = router;
3. 修正前端Post.js的token存储、传递逻辑
首先修正登录时的token存储逻辑,必须从响应体中取accessToken字段再存入sessionStorage,禁止直接存储整个响应对象。发请求时使用标准Authorization头传递token:
// 登录请求的成功回调中,按如下方式存储token // axios.post("http://localhost:4000/auth/login", { username, password }) // .then(res => { // // 精准取token字段存储,不要直接存res.data // sessionStorage.setItem("accessToken", res.data.accessToken) // }) // Post.js中addComment方法的请求部分修改如下 const addComment = () => { const accessToken = sessionStorage.getItem('accessToken') if (!accessToken) { alert("请先登录后再评论") return } axios.post(`http://localhost:4000/comments`, { commentBody: newComment, PostId: id }, { headers: { // 使用标准Authorization头,携带Bearer前缀 Authorization: `Bearer ${accessToken}`, } } ) .then((res) => { const data = res.data; setComments([...comments, data]) setNewComment("") }) .catch((err) => { alert("评论提交失败,请重试") }) }
额外注意事项
- 所有接口返回错误时要匹配对应的HTTP状态码:未登录用401、token无效用403、资源不存在用404,不要统一返回200状态码,不利于前端做全局异常拦截。
- 生产环境不要将JWT签名密钥硬编码在代码中,需通过环境变量注入。
- 跨域场景下要配置允许的请求头,避免自定义头被跨域策略拦截。
内容的提问来源于stack exchange,提问作者Sharanjeet Singh
相关产品推荐
相关产品推荐

