我是小ao。在"面试克星"项目里,我需要用 JWT 保护用户更新接口,确保用户只能改自己的信息。之前在 Vue 论坛项目中,我用 JavaScript 写过类似的中间件,跑得很顺畅。但这次换到 TypeScript + Express,我本以为只是多写几个类型,结果连踩三个坑。
这篇文章,就是我从头实现一个 JWT 鉴权中间件的实战记录。
中间件要做什么?
我的中间件逻辑很简单:
- 从请求头取出
Authorization字段,格式必须是Bearer <token>。 - 用
jsonwebtoken库验证 token 的合法性。 - 验证通过后,从数据库查出用户信息,挂载到
req对象上。 - 后面的路由处理函数就能直接从
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 里可以靠编译器帮你记住。