Как исключить поле сущности из возвращаемого контроллером JSON. NestJS + Typeorm

Я хочу исключить поле пароля из возвращенного JSON. Я использую NestJS и Typeorm.

Решение, предоставленное по этим вопросам, не работает ни для меня, ни в NestJS -> Можно ли "защитить" свойство и исключить его из операторов выбора. Я могу опубликовать свой код, если это необходимо. Любые другие идеи или решения? Благодарю.

13 ответов

Решение

Я бы предложил создать перехватчик, который бы использовал библиотеку преобразователей классов:

@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;
}

Вы можете перезаписать метод 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, но будет доступен в экземпляре модели. Мне нравится это решение, потому что оно сохраняет всю конфигурацию модели в объекте.

В этой ветке много хороших ответов. Чтобы опираться на ответ apun выше, я думаю, что следующий подход с наименьшей вероятностью приведет к случайной утечке поля пароля:

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

Если объект не выбирает это поле по умолчанию, и его можно запросить только явно (например, через addSelect() если вы используете построитель запросов), я думаю, что гораздо меньше вероятность того, что где-то есть промах, и меньше полагаться на "магию" фреймворка (которая в конечном итоге class-transformerбиблиотека) для обеспечения безопасности. На практике во многих проектах единственное место, где вы явно выбираете, - это проверка учетных данных.

Этот подход также может помочь предотвратить случайное попадание хэша пароля в записи журнала и т. Д., О чем еще не упоминалось. Гораздо безопаснее бросать пользовательский объект, зная, что он не содержит конфиденциальной информации, особенно если он может быть сериализован где-нибудь в записи журнала.

В общем, документированный подход для NestJS заключается в использовании @Exclude() декоратор и принятый ответ от основателя проекта.

Я определенно часто использую Exclude() декоратор, но не обязательно для полей пароля или соли.

В дополнение к ответу Камиля:

Вместо того, чтобы создавать свой собственный перехватчик, теперь вы можете использовать встроенный ClassSerializerInterceptor смотрите документацию по сериализации.

@UseInterceptors(ClassSerializerInterceptor)

Вы можете использовать его в классе контроллера или его отдельных методах. Каждая сущность, возвращаемая таким методом, будет преобразована с помощью класса-трансформера.

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

О скрытых столбцах можно прочитать здесь

то, что просто работало для меня, было просто

аннотирование поля с помощью @Exclude и переопределение метода модели toJSON, например

toJSON() { return classToPlain(this); }

Вы можете использовать пакет https://github.com/typestack/class-transformer

Вы можете исключить свойства с помощью декораторов, а также вы можете исключить свойства с помощью групп.

Это уже старая тема, но я все же хотел бы поделиться своим решением, может, это кому-то поможет. Я использую Express, но мой пример, наверное, подходит и для этого случая.

Итак, в своем классе сущности вы просто определяете дополнительный статический removePassword() , который получает экземпляр самой сущности, и вы отправляете объект, созданный этим методом, вместо исходного объекта сущности, полученного из БД:

      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() метод).

Хотя в обработчиках маршрутов вы всегда будете делать что-то вроде этого:

      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());

Чтобы избежать болей в спине и головных болей, я бы посоветовал использовать plainToClass чтобы иметь полную совместимость мангуста / преобразования классов и избежать необходимости делать пользовательские переопределения для преодоления этой проблемы.

Например, добавьте это в свой сервис:

      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

Тот, кто столкнется с этим вопросом в будущем, может также рассмотреть возможность использования подхода, который я предложил здесь: https://stackoverflow.com/a/75436936/4397899 .

      @Injectable()
export default class TransformInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<AnyObject> {
    return next.handle().pipe(map((args) => instanceToPlain(args)));
  }
}

Также - есть документация по этому поводу https://docs.nestjs.com/techniques/serialization

В дополнение к этому ответу для Nestjs.

Как использовать группы классов-преобразователей динамически, поскольку группы фиксируются в декораторе функций. Поместите динамические группы в или.

      @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']});
}

В classToClass а также plainToClass внутри тела функции выполняет фактическую фильтрацию, пока @SerializeOptions чтобы убедиться, что отфильтрованные данные будут возвращены правильно.

Другие вопросы по тегам