Express + TypeScript 下写 JWT 中间件,我踩了三个坑

我是小ao。在"面试克星"项目里,我需要用 JWT 保护用户更新接口,确保用户只能改自己的信息。之前在 Vue 论坛项目中,我用 JavaScript 写过类似的中间件,跑得很顺畅。但这次换到 TypeScript + Express,我本以为只是多写几个类型,结果连踩三个坑。

这篇文章,就是我从头实现一个 JWT 鉴权中间件的实战记录。

中间件要做什么?

我的中间件逻辑很简单:

  1. 从请求头取出 Authorization 字段,格式必须是 Bearer <token>
  2. jsonwebtoken 库验证 token 的合法性。
  3. 验证通过后,从数据库查出用户信息,挂载到 req 对象上。
  4. 后面的路由处理函数就能直接从 req.user 拿到当前登录用户。

在 JS 版本里,我直接在 req 上挂了一个 user 属性,什么事都没有。但到了 TS 里,编译器直接报错:类型"Request"上不存在属性"user"

第一个坑:扩展 Express 的 Request 类型

Express 的类型定义里,Request 对象没有 user 属性。我需要告诉 TypeScript:"我的 req 上会多一个 user 对象"。

我首先尝试了最"优雅"的做法------创建一个 src/types/express.d.ts 文件,用 declare global 来扩展 Express.Request。谁知不管怎么配,编译时就是找不到这个属性。后来发现,只要 .d.ts 文件里包含 import 语句,TypeScript 就会把它当成普通模块,全局扩展失效。

折腾了一下午,我选择了最务实的方法:直接在中间件文件 auth.ts 里定义一个扩展接口 AuthRequest extends Request,并导出它。在路由文件里导入并使用:

typescript 复制代码
export interface AuthRequest extends Request {
  user?: any;
}

在路由里,我显式声明 req: AuthRequest,TypeScript 终于安静了。优雅败给了现实,但代码跑通了。

第二个坑:req.user 可能为 undefined

扩展后,TS 不再报错"属性不存在",但它在使用 req.user._id 时依旧会警告:"对象可能为'未定义'"。因为 user 被我定义成了可选属性(user?),而 TypeScript 不知道中间件已经在运行时给它赋了值。

解决方案是加非空断言 !req.user!._id。这行代码的意思是:"我确定这个值在这里一定不是 nullundefined,你不用检查了"。它不是偷懒,而是一种与编译器的"约定":中间件保证了这个值存在,请放心。

第三个坑:Token 应该存在哪儿?

Token 的存储位置一直是个争议点。放在 localStorage 里,有被 XSS 攻击窃取的风险;放在 httpOnly cookie 里更安全,但需要后端配合设置,实现成本高。

考虑到这还是个人开发阶段,而且我之前论坛项目的 JWT 也是一直存在 localStorage,我决定暂时沿用这个方案。安全性与开发成本的权衡,没有银弹,只有适合当前阶段的方案。

完整的中间件实现

下面是最终能正常工作的 authMiddleware。它从 authHeader 中提取 token,用 jwt.verify 解析,查数据库,挂载用户,最后调用 next()。如果 token 无效或过期,会返回不同的错误信息。

typescript 复制代码
export const authMiddleware = async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ message: '未授权' });
    }
    const token = authHeader.split(' ')[1];
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
    const user = await User.findById(decoded.userId).select('-password');
    if (!user) return res.status(401).json({ message: '用户不存在' });
    req.user = user;
    next();
  } catch (error) {
    // 处理 Token 过期、无效等情况
  }
};

写在最后

TypeScript 给后端开发带来的"严格",刚开始时确实会拖慢节奏。但当你习惯了先定义类型、再实现逻辑的流程后,会发现很多运行时的错误在编译阶段就被消灭了。这次踩坑经历让我更清楚地认识到:在 JS 里靠经验避免的错误,在 TS 里可以靠编译器帮你记住。

相关推荐
kyriewen19 小时前
别再对着 TypeScript 报错发呆了:我把 10 个最常见的红色波浪线翻译成了人话
前端·javascript·typescript
见过夏天1 天前
Node.js 常用命令全攻略
node.js
妙码生花1 天前
现代前端的极致性能 icon 加载方案(死磕成功版)
前端·vue.js·typescript
MonkeyKing1 天前
鸿蒙ArkTS深度剖析:ArkTS与TS/JS核心差异、静态强类型实战优势
typescript·harmonyos
前端双越老师1 天前
我从 0 开发的 AI Agent 智语项目发布了
前端·node.js·agent
kyriewen2 天前
2026 年了,还在用 Node.js?Bun 迁移实战:20 分钟搞定,附踩坑记录
前端·javascript·node.js
donecoding2 天前
3 条命令搞定闭环 Monorepo:Lerna 版本管理 + 拓扑构建 + 自定义分发
前端·前端框架·node.js
Momo__3 天前
TypeScript satisfies 操作符——比 as 更安全的类型守门员
前端·typescript
Flynt3 天前
npm v12 来了:allowScripts 默认关闭,我的项目差点跑不起来
安全·npm·node.js
森鹿3 天前
express中间件原理以及大致实现
前端·express