【问题标题】:How to exclude entity field from returned by controller JSON. NestJS + Typeorm如何从控制器 JSON 返回的实体字段中排除。 NestJS + Typeorm
【发布时间】:2018-10-25 20:55:23
【问题描述】:

我想从返回的 JSON 中排除密码字段。 我正在使用 NestJS 和 Typeorm。

this question 上提供的解决方案不适用于我或 NestJS。如果需要,我可以发布我的代码。 还有其他想法或解决方案吗?谢谢。

【问题讨论】:

    标签: node.js typescript nestjs typeorm


    【解决方案1】:

    你可以使用包https://github.com/typestack/class-transformer

    您可以使用装饰器排除属性,也可以使用组排除属性。

    【讨论】:

    • Silva,您能否指出对“使用组排除属性”的引用?我找不到它。谢谢
    【解决方案2】:

    我建议创建一个利用 class-transformer 库的拦截器:

    @Injectable()
    export class TransformInterceptor implements NestInterceptor {
      intercept(
        context: ExecutionContext,
        call$: Observable<any>,
      ): Observable<any> {
        return call$.pipe(map(data => classToPlain(data)));
      }
    }
    

    然后,只需使用@Exclude() 装饰器排除属性,例如:

    import { Exclude } from 'class-transformer';
    
    export class User {
        id: number;
        email: string;
    
        @Exclude()
        password: string;
    }
    

    【讨论】:

    • 这是@kamil-myśliwiec :-D 创作者的最佳答案。
    • 谢谢你,@kamil-myśliwiec。我会尽快试试这个。
    • 此外,您可以在数据库查询期间(在 ORM 或 SQL 查询中)排除字段。 TypeORM 示例 - github.com/typeorm/typeorm/issues/535 .
    • 我不明白如何在嵌套中实现 TransformInterceptor 类 :( ?
    • 如果我没记错这里可以使用内置的ClassSerializerInterceptor
    【解决方案3】:

    作为Kamil's answer的补充:

    您现在可以使用内置的ClassSerializerInterceptor,而不是创建自己的拦截器,参见serialization docs

    @UseInterceptors(ClassSerializerInterceptor)
    

    您可以在控制器类或其各个方法上使用它。这种方法返回的每个实体都将使用 class-transformer 进行转换,因此将 @Exclude 注释考虑在内:

    import { Exclude } from 'class-transformer';
    
    export class User {
        /** other properties */    
    
        @Exclude()
        password: string;
    }
    

    您可以通过在控制器或其方法上定义 @SerializeOptions() 来自定义其行为:

    @SerializeOptions({
      excludePrefixes: ['_'],
      groups: ['admin']
    })
    

    例如,仅向某些用户公开某些字段:

    @Expose({ groups: ["admin"] })
    adminInfo: string;
    

    【讨论】:

    • 你不会碰巧知道内置类是否支持定义组,即@Expose({ groups: ["user", "admin"] })?我一直在努力弄清楚您将如何指示应该从控制器中使用哪个组。从class-transformer 文档我可以看到它支持classToPlain(user, { groups: ["user"] }),但我有点困惑nest 在哪里调用它,以及是否可以通过这些选项。
    • 只是一个新手问题:您在哪里定义“组”,即 class-transformer 如何知道用户所在的组?我们应该先定义组模型并让用户有多个组有点关系吗?即用户将拥有 user.groups = ['editor', user']?那么我们如何将当前请求的用户传递给拦截器,以便它相应地显示/隐藏字段呢?
    • @AhmetCetin SerializeOptions 仅在 groups 是每个端点的静态时才有效。如果要根据请求动态确定groups,则必须为此实现自定义拦截器。
    • 查看这个答案,其中组是动态确定的以进行验证,您需要做类似的事情,但要进行序列化(在您的情况下是拦截器而不是管道):stackoverflow.com/a/54057206/4694994
    • @ddsultan 请对此提出一个新问题;如果没有更多详细信息,就无法在 cmets 中调试您的问题:一个最小的示例,以便可以重现问题以及您的预期行为或输出。谢谢:-)
    【解决方案4】:

    你可以像这样覆盖模型的 toJSON 方法。

    @Entity()
    export class User extends BaseAbstractEntity implements IUser {
      static passwordMinLength: number = 7;
    
      @ApiModelProperty({ example: faker.internet.email() })
      @IsEmail()
      @Column({ unique: true })
      email: string;
    
      @IsOptional()
      @IsString()
      @MinLength(User.passwordMinLength)
      @Exclude({ toPlainOnly: true })
      @Column({ select: false })
      password: string;
    
      @IsOptional()
      @IsString()
      @Exclude({ toPlainOnly: true })
      @Column({ select: false })
      passwordSalt: string;
    
      toJSON() {
        return classToPlain(this);
      }
    
      validatePassword(password: string) {
        if (!this.password || !this.passwordSalt) {
          return false;
        }
        return comparedToHashed(password, this.password, this.passwordSalt);
      }
    }
    

    通过使用 plainToClass 的类转换器方法和 @Exclude({ toPlainOnly: true }),密码将从 JSON 响应中排除,但将在模型实例中可用.我喜欢这个解决方案,因为它将所有模型配置保留在实体中。

    【讨论】:

    • 有没有关于 toJSON() 的文档?
    • toJSON 方法是可以在任何 javascript 类中重写的方法。它定义了如何将类转换为常规对象。调用 JSON.stringify 时,会调用该类的 toJSON 方法。 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… 见描述部分。
    • 只有添加 @Exclude({ toPlainOnly: true }) 才是我想要的
    【解决方案5】:
    @Column({ select: false })
    password: string
    

    可以阅读关于隐藏列here

    【讨论】:

    • 请不要只发布代码作为答案,还要解释您的代码的作用以及它如何解决问题的问题。带有解释的答案通常更有帮助,质量更高,更有可能吸引投票。
    • 当然。我认为这是不言自明的。
    【解决方案6】:

    这个帖子中有很多好的答案。为了建立上面apun的答案,我认为以下方法最不可能意外泄漏密码字段:

    @Column({ select: false })
    password: string
    

    如果实体默认不选择该字段,并且只能显式查询(例如,如果使用查询生成器,则通过addSelect()),我认为某处出现滑倒的可能性要小得多,并且更少依赖框架的“魔力”(最终是class-transformer 库)来确保安全性。实际上,在许多项目中,您唯一明确选择的地方就是您检查凭据的地方。

    这种方法还可以帮助防止密码哈希意外泄漏到日志条目等中,这是一个尚未提及的考虑因素。知道它不包含敏感信息,尤其是如果它最终可能在某个日志条目中序列化,那么在用户对象周围折腾感觉更安全。

    总而言之,NestJS 的记录方法是使用 @Exclude() 装饰器,并且接受的答案来自项目的创始人。

    我肯定经常使用 Exclude() 装饰器,但不一定用于密码或盐字段。

    【讨论】:

      【解决方案7】:

      这已经是一个老话题了,但我仍然想分享我的解决方案,也许它会对某人有所帮助。我使用 Express,但我的示例可能也适合这种情况。

      因此,在您的实体类中,您只需定义一个额外的静态 removePassword() 方法,该方法接收实体本身的实例,然后您发送一个由该方法创建的对象,而不是您从 DB 获得的原始实体对象:

      import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
      
      @Entity({ name: 'users' })
      export class User {
        @PrimaryGeneratedColumn('uuid')
        id: string | undefined;
      
        @Column({ type: 'varchar', length: 100, unique: true })
        email: string | undefined;
      
        @Column({ type: 'text' })
        password: string | undefined;
      
        static removePassword(userObj: User) {
          return Object.fromEntries(
            Object.entries(userObj).filter(([key, val]) => key !== 'password')
          );
        }
      }
      

      这基本上就像在数组上调用filter() 方法,但稍微复杂一些:您从条目数组中创建一个新对象(您最终将发送的对象),该对象是通过从原始条目中过滤掉密码条目而产生的数组(使用这种精确的filter() 方法)。

      在你的路由处理程序中,虽然你总是会做这样的事情:

      import { Router, Request, Response, NextFunction } from 'express';
      import { User } from '../../entity/User';
      import { getRepository } from 'typeorm';
      
      const router = Router();
      
      router.post(
        '/api/users/signin',
        (req: Request, res: Response, next: NextFunction) => {
          const { email } = req.body;
      
          getRepository(User)
            .findOne({ email })
            .then(user =>
              user ? res.send(User.removePassword(user)) : res.send('No such user:(')
            )
            .catch(err => next(new Error(err.message)));
        }
      );
      
      export { router as signinRouter };
      

      您也可以使用常规方法:

      withoutPassword() {
        return Object.fromEntries(
          Object.entries(this).filter(([key, val]) => key !== 'password')
        );
      }
      

      在你的路由处理程序中:

      res.send(user.withoutPassword());
      

      【讨论】:

        【解决方案8】:

        为了避免任何背痛和头痛, 我建议使用 plainToClass 来获得完整的 mongoose/class-transform 兼容性,并避免必须进行自定义覆盖来克服这个问题。

        例如,将其添加到您的服务中:

        async validateUser(email: string, password: string): Promise<UserWithoutPassword | null> {
            const user = await this.usersService.findOne({ email });
        
            if (user && await compare(password, user.password))
            {
                return plainToClass(UserWithoutPassword, user.toObject());
            }
        
            return null;
        }
        
        

        来源:Stackoverflow answer

        【讨论】:

          【解决方案9】:

          对我有用的只是

          使用 @Exclude 注释字段并像这样覆盖 toJSON 模型方法

          toJSON() { return classToPlain(this); }

          【讨论】:

            【解决方案10】:

            除了this answer for Nestjs。

            如何以动态方式使用类转换器组,因为这些组在函数装饰器@SerializeOptions 中是固定的。将动态组放入classToClassplainToClass

            @Post()
            @SerializeOptions({
              groups: ['admin','user'],
            })
            async create(
              @Body() createProjectDto: CreateProjectDto,
            ) {
              const data = await this.projectsService.create(createProjectDto);
              return classToClass(data, { groups: ['admin','DYNAMIC_GROUP'] });
              //Or you can return
              //return plainToClass(Project, plainObject, {groups: ['admin','DYNAMIC_GROUP']});
            }
            

            函数体内的classToClassplainToClass进行实际过滤,而@SerializeOptions确保过滤后的数据将正确返回。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2018-07-28
              • 1970-01-01
              • 2023-02-13
              • 1970-01-01
              • 1970-01-01
              • 2020-01-27
              • 2020-06-02
              相关资源
              最近更新 更多