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

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

Решение, представленное на этот вопрос не работает ни у меня, ни в NestJS. При необходимости я могу опубликовать свой код. Есть другие идеи или решения? Спасибо.


person Vladyslav Moisieienkov    schedule 15.05.2018    source источник


Ответы (10)


Я бы предложил создать перехватчик, который использует преимущества библиотеки 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;
}
person Kamil Myśliwiec    schedule 16.05.2018
comment
Это ti @ kamil-myśliwiec :-D Лучший ответ для создателя. - person João Silva; 16.05.2018
comment
Спасибо, @ kamil-myśliwiec. Я попробую это как можно скорее. - person Vladyslav Moisieienkov; 20.05.2018
comment
Также вы можете исключить поля во время запроса к базе данных (в ORM или SQL-запросе). Пример для TypeORM - github.com/typeorm/typeorm/issues/535. - person Vladyslav Moisieienkov; 29.05.2018
comment
Я не понимаю, как реализовать класс TransformInterceptor в гнезде :(? - person ZecKa; 26.04.2020
comment
Здесь можно использовать встроенный ClassSerializerInterceptor, если я не ошибаюсь - person Jemar Jones; 30.12.2020
comment
Есть ли способ сделать это с помощью призмы? Prisma, похоже, вообще не использует поток сущностей :( - person Mickey Sly; 09.06.2021

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

person Francisco Javier Sucre Gonzále    schedule 02.12.2019
comment
есть ли какой-нибудь документ о toJSON ()? - person Cheney Shi; 21.01.2021
comment
toJSON - это метод, который можно переопределить в любом классе javascript. Он определяет, как класс преобразуется в обычный объект. При вызове JSON.stringify вызывается метод toJSON класса. developer.mozilla.org/en-US/ docs / Web / JavaScript / Reference / См. раздел описания. - person Francisco Javier Sucre Gonzále; 03.02.2021
comment
Я искал только добавление @Exclude ({toPlainOnly: true}) - person RicardoVallejo; 04.02.2021

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

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

@UseInterceptors(ClassSerializerInterceptor)

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

import { Exclude } from 'class-transformer';

export class User {
    /** other properties */    

    @Exclude()
    password: string;
}

Вы можете настроить его поведение, определив @SerializeOptions() на вашем контроллере или его методах:

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

чтобы, например, предоставить определенные поля только определенным пользователям:

@Expose({ groups: ["admin"] })
adminInfo: string;
person Kim Kern    schedule 20.01.2019
comment
Вы случайно не знаете, поддерживает ли встроенный класс определение групп, например @Expose({ groups: ["user", "admin"] })? Я изо всех сил пытался понять, как вы могли бы указать, какую группу следует использовать в контроллере. Из class-transformer документации я вижу, что он поддерживает classToPlain(user, { groups: ["user"] }), но я немного запутался, где его вызывает nest, и могу ли я пройти через эти параметры. - person Jamie Street; 03.04.2019
comment
просто вопрос новичка: где вы определяете группы, т.е. как преобразователь классов узнает, в какой группе находится пользователь? Должны ли мы сначала определить модель группы и сделать так, чтобы у пользователя было какое-то отношение к нескольким группам? то есть у пользователя будет user.groups = ['editor', user ']? Тогда как нам передать пользователя текущего запроса перехватчику, чтобы он соответственно отображал / скрывал поля? - person Ahmet Cetin; 26.05.2020
comment
@AhmetCetin SerializeOptions работает, только если groups статичны для каждой конечной точки. Если вы хотите динамически определять groups на основе запроса, вы должны реализовать для этого собственный перехватчик. - person Kim Kern; 26.05.2020
comment
См. Этот ответ, где группы динамически определяются для проверки, вам нужно сделать что-то подобное, но для сериализации (в вашем случае перехватчик вместо трубы): stackoverflow.com/a/54057206/4694994 - person Kim Kern; 26.05.2020
comment
@KimKern, @Expose не работал должным образом на уровне метода по запросу POST. Я использую nestjs 6.5.3 - person ddsultan; 21.12.2020
comment
@ddsultan Пожалуйста, откройте новый вопрос по этому поводу; невозможно отладить вашу проблему в комментариях без дополнительных деталей: минимальный пример, чтобы можно было воспроизвести проблему и ваше ожидаемое поведение или результат. Спасибо :-) - person Kim Kern; 21.12.2020

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

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

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

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

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

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

person firxworx    schedule 17.07.2020

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

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

person João Silva    schedule 16.05.2018
comment
Сильва, не могли бы вы указать ссылку на исключение свойств с помощью групп? Я не смог его найти. Спасибо - person ddsultan; 21.12.2020

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

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

person infinite    schedule 21.06.2020
comment
Пожалуйста, не публикуйте только код в качестве ответа, но также объясните, что делает ваш код и как он решает проблему вопроса. Ответы с объяснением обычно более полезны и качественнее, и с большей вероятностью получат положительные отзывы. - person Mark Rotteveel; 21.06.2020
comment
Конечно. Я думал, что это говорит само за себя. - person infinite; 22.06.2020

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

person Paul Paca-Vaca Seleznev    schedule 28.07.2020

Это уже старая тема, но я все же хотел бы поделиться своим решением, может быть, это кому-то поможет. Я использую 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() в массиве, но немного сложнее: вы создаете новый объект (тот, который вы в конечном итоге отправите) из массива записей, созданный путем фильтрации записи пароля из исходного массива записей (с этим точным 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());
person b.niki    schedule 15.05.2021

Чтобы избежать болей в спине и головных болей, я бы предложил использовать 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

person Sebastien H.    schedule 25.05.2021

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

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

toJSON() { return classToPlain(this); }

person Aminu Zack    schedule 11.06.2021