NestJS认证模块ES6解构Mongoose对象无法移除password问题
和Promise机制没有任何关系,核心原因是Mongoose查询返回的user不是普通JavaScript对象,而是Mongoose封装的Document实例。
你看到的$__、$isNew、_doc都是Document实例自身的内部属性,平时写代码能直接通过user.password、user.email访问到业务字段,是因为Mongoose在Document原型层做了getter/setter代理,真实的业务数据实际存在内部的_doc字段上。
ES6解构只会遍历对象自身的可枚举属性,不会读取原型链上通过代理挂载的属性,所以你解构出来的全是Mongoose的内部状态字段,真正的用户数据还留在_doc里,自然也没法通过解构移除password——你从一开始就没解构到这个被代理的字段。
官方示例用的是内存中存储的普通用户对象数组,不存在Mongoose Document的代理逻辑,所以解构写法在示例里可以正常运行,你直接套用到Mongoose场景就会出现这个问题。
必须处理,不要有侥幸心理:
- 哪怕当前接口最终只返回带用户_id的JWT,不直接响应用户对象,只要带密码哈希的Document实例在业务链路中传递,后续代码迭代时很容易因为疏忽把整个对象序列化返回,直接造成敏感数据泄露
- 即便是bcrypt这类高强度哈希的密码,泄露后依然存在被暴力碰撞的风险,本身就不该脱离认证环节流转
- 敏感数据按需返回是基本的安全原则,没必要在非必要的环节携带密码字段。
不要直接访问内部的_doc字段,这是Mongoose的私有属性,版本迭代可能变更,业务代码依赖私有属性会留下兼容隐患,推荐按优先级选以下正规方案:
- 最高优先级:查询时直接返回普通JS对象
在Mongoose查询后链式调用.lean()方法,返回结果就是纯普通JS对象,没有Mongoose的内部属性和代理逻辑,解构、属性删除等操作都和普通对象行为一致,同时查询性能比返回Document实例高3-5倍,是最推荐的写法:
注意如果是async findOneByEmail(email: string): Promise<User | undefined> { return await this.userModel.findOne({ email: email }).lean().exec(); }validateUser这类需要比对密码的场景,先正常查询带密码的用户,比对通过后,对lean返回的普通对象直接解构或者delete掉password字段即可。 - 根源规避:查询层面直接排除敏感字段
对于不需要密码的查询场景,直接在查询时过滤掉password字段,从根源避免敏感字段被查出:
也可以在Schema定义时给password字段加// 第二个参数为字段投影,-password表示排除password字段 return await this.userModel.findOne({ email: email }, '-password').lean().exec();select: false配置,默认所有查询都不会返回password字段,需要做密码比对的时候再显式通过.select('+password')查出来,一劳永逸。 - 兼容场景:手动转换Document为普通对象
如果某些场景必须使用Mongoose Document实例,可以在做属性操作前调用实例自带的.toObject()或.toJSON()方法,转为普通JS对象后再做解构、字段删除操作:const user = await this.usersService.findOne(username); if (user && user.password === pass) { const plainUser = user.toObject(); const { password, ...result } = plainUser; return result; }
既不是NestJS的bug,也不是Mongoose的bug,属于典型的ODM使用不当:
官方示例使用内存模拟数据,所有用户对象都是原生普通JS对象,解构逻辑可以正常运行;你在切换为Mongoose作为存储层时,没有处理Mongoose Document和原生JS对象的行为差异,直接照搬了普通对象的写法才触发问题,是使用Mongoose开发时非常常见的入门踩坑点。
内容的提问来源于stack exchange,提问作者Liam Clark Gutiérrez

