You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js校验前端传入JWT报must be string/malformed错误

问题现象

React前端传递JWT到Node.js后端鉴权过程中先后抛出两类错误:

  • 未做类型转换时,报错:JsonWebTokenError: jwt must be a string
  • 手动对获取到的token调用toString()转换后,报错变为:JsonWebTokenError: jwt malformed
    排查确认前端传入的accessToken到达鉴权中间件时被识别为对象类型。
根因分析

核心错误共3处,按影响优先级排序:

  1. Express中间件参数顺序写反:Express中间件参数固定顺序为(req, res, next),现有代码写为(res, req, next),导致实际调用res.header()尝试获取请求头——res是响应对象,本身不存在获取请求头的能力,拿到的是对象类型的内置属性/方法,这是触发错误的根本原因。对对象调用toString()会得到固定字符串[object Object],完全不符合JWT的三段式格式,自然触发格式错误。
  2. 登录接口返回格式不规范:直接将token字符串作为整个响应体返回,前端如果未做精准取值直接存储,很容易存入非字符串格式的内容。
  3. 请求头传递不规范:自定义请求头未遵循通用约定,也未使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 13:03:21