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。这行代码的意思是:"我确定这个值在这里一定不是 null 或 undefined,你不用检查了"。它不是偷懒,而是一种与编译器的"约定":中间件保证了这个值存在,请放心。

第三个坑: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 里可以靠编译器帮你记住。

相关推荐
二哈赛车手1 小时前
新人笔记---hermes定时功能设计,具体讲解以及可优化点
java·人工智能·笔记·spring·ai·typescript
jktantan3 小时前
每个节点 41 字节:把一棵百万节点树拆进 14 个 typed array
前端·javascript·vue.js·算法·typescript
YIAN5 小时前
RAG 学到最后,我真正学会的不是“检索”,而是让系统知道自己缺什么
langchain·node.js·llm
用户11977079480735 小时前
GitHub 电商开源项目分享:用 sharp 批量出饰品主图,试了裁剪、细节图和色彩配置文件三个坑
前端·node.js
donecoding6 小时前
pnpm 配好了,为什么执行的还是另一份?命令入口与运行时排查
node.js·前端工程化
江屿风6 小时前
【Plain Language Large Model】【从浏览器到全栈:一课精通Node.js三剑客】流食般投喂
笔记·前端框架·npm·node.js·js·npx
Nymph_Zhu7 小时前
vue3+element-plus 文件上传
前端·typescript·vue3·element-plus
QianhangQianping7 小时前
撤销重做不是「存快照」:一个可视化编辑器的Operation 设计
java·前端·数据库·typescript·编辑器·前端架构·撤销重做
SomeOtherTime9 小时前
node.js基础(AI回答)
node.js
用户0934077735141 天前
HarmonyOS WPS Open SDK 工程实践:把 registerApp 做成就绪门禁
typescript·harmonyos