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