- Оригинальный адрес:NestJS Basic Auth and Sessions
- Оригинальный автор:Just Another Typescript Blog
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:Jessica
- Корректор:samyu2000
NestJS реализует базовую аутентификацию пользователей и сеансы.
Отказ от ответственности за использование кода
Весь код на этом сайте является свободным программным обеспечением: вы можете распространять или изменять его в соответствии с условиями Стандартной общественной лицензии GNU, опубликованной Free Software Foundation. Где лицензия версии 3 или (на ваш выбор) любой более новой версии.
Надеюсь, весь код в статье вам поможет, нобез каких-либо гарантий. Также нет никакой гарантии, что код будет работать и что он будет работать для определенной функции. Дополнительные сведения см. в Стандартной общественной лицензии GNU.
В этой статье рассматриваются NestJS и политики аутентификации, а также документируется мой процесс реализации политик аутентификации в NestJS с использованием моих знаний Node.Однако это не означает, что вы должны делать это так, как я делаю в реальных проектах..
В этой статье мы рассмотрим, какNestJSиспользуется вpassport.jsдля простой реализации базовой аутентификации и управления сеансами.
Во-первых, клонируйте этот предварительно настроенный начальный проект из github, гдеpackage.jsonФайл содержит все библиотеки, необходимые для этого проекта, а затем выполнитьnpm install.
- GitHub.com/AR Тонио/РЭШ…
- GitHub.com/AR Тонио/РЭШ…- Полный исходный код проекта.
В этом проекте будут использоваться следующие методы и библиотеки.
- Swagger- Он может генерировать окончательную документацию соответствующего интерфейса REST API для вашего приложения. Это также отличный инструмент для быстрого тестирования API. Вы можете прочитать о Swagger на веб-сайте NestJS.Документация, чтобы использовать Swagger в нашем проекте.
- Exception Filters- Это встроенный модуль обработки исключений NestJS, который отвечает за обработку всех исключений, возникающих во всем приложении. Когда приложение перехватывает необработанное исключение, пользователь получает дружественный и достойный ответ. Это означает, что любое исключение, которое мы сгенерируем в любом месте приложения, будет перехвачено глобальным обработчиком исключений и вернет предопределенный ответ JSON.
- TypeORM- Это очень надежный и зрелый фреймворк ORM, хотя и не так давно. Он написан на TypeScript. Также поддерживаетActiveRecord и DataMapperрежиме, но также поддерживает кэширование и многие другие функции. Его документация также превосходна. TypeORM поддерживает большинство баз данных SQL и NoSQL. Для этого проекта мы будем использовать базу данных sqlite. И используйте схему ActiveRecord.TypeORM TypeDocs (похож на javadocs)
- Custom Decorator- Мы создадим собственный декоратор маршрута для доступа к пользовательскому объекту в сеансе.
- Basic Auth — аутентификация пользователя с использованием заголовка Basic Auth.
- Sessions- После аутентификации пользователя создаются сеанс и файл cookie, чтобы при каждом запросе, требующем информации о пользователе, мы могли получить доступ к вошедшему в систему пользователю из объекта сеанса.
схема базы данных
мы собираемся создать.Схема этого проекта проста. У нас много пользователей и проектов, но пользователь может сопоставить только соответствующий ему проект. Мы хотим иметь возможность войти в систему с учетными данными пользователя, которые соответствуют записи в базе данных, и после входа в систему мы будем использовать файл cookie для получения элемента для пользователя.
особенности дизайна.Создать пользователя; создать проект для вошедшего в систему пользователя; получить всех пользователей; получить все проекты вошедшего в систему пользователя. В этом проекте нет объектов для обновления или удаления.
Структура проекта
общий каталог:Пользовательские исключения и фильтры исключений.
каталог проекта:служба проекта, контроллер проекта, объект базы данных проекта, модуль проекта.
каталог пользователя:пользовательская служба, пользовательский контроллер, объект пользовательской базы данных, пользовательский модуль.
каталог авторизации:AppAuthGuard, сериализатор/десериализатор файлов cookie, политика HTTP, защита сеансов, служба аутентификации, модуль аутентификации.
Создайте пользовательский модуль
Условие: @nest/cli должен быть установлен глобально.
Создайте пользовательский модуль
nest g mo user
Это создаст пользовательский каталог и пользовательский модуль.
Создайте пользовательский контроллер
nest g co user
Это помещает пользовательский контроллер в пользовательский каталог и обновляет пользовательский модуль.
Создать пользовательский сервис
nest g s user
Это создаст пользовательский сервис и обновит пользовательский модуль. Но мой пользовательский сервис в конечном итоге помещается в корневую папку проекта, а не в пользовательскую папку, в чем я не уверен.Ошибка или особенность фреймворка Nestjs? Если это произойдет и с вами, вручную переместите его в пользовательскую папку и обновите ссылочный путь пользовательской службы в пользовательском модуле.
Создать пользовательский объект
import {BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn} from 'typeorm';
import * as crypto from 'crypto';
import {ProjectEntity} from '../project/project.entity';
import {CreateUserDto} from './models/CreateUserDto';
import {AppErrorTypeEnum} from '../common/error/AppErrorTypeEnum';
import {AppError} from '../common/error/AppError';
@Entity({name: 'users'})
export class UserEntity extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 30
})
public firstName: string;
@Column({
length: 50
})
public lastName: string;
@Column({
length: 50
})
public username: string;
@Column({
length: 250,
select: false,
name: 'password'
})
public password_hash: string;
set password(password: string) {
const passHash = crypto.createHmac('sha256', password).digest('hex');
this.password_hash = passHash;
}
@OneToMany(type => ProjectEntity, project => project.user)
projects: ProjectEntity[];
public static async findAll(): Promise<UserEntity[]> {
const users: UserEntity[] = await UserEntity.find();
if (users.length > 0) {
return Promise.resolve(users);
} else {
throw new AppError(AppErrorTypeEnum.NO_USERS_IN_DB);
}
}
public static async createUser(user: CreateUserDto): Promise<UserEntity> {
let u: UserEntity;
u = await UserEntity.findOne({username: user.username});
if (u) {
throw new AppError(AppErrorTypeEnum.USER_EXISTS);
} else {
u = new UserEntity();
Object.assign(u, user);
return await UserEntity.save(u);
}
}
}
Вот несколько заметок о UserEntity. При установке свойства пароля мы будем использовать TypeScriptsetter, и хеширует наш пароль. В этом файле мы используем AppError и AppErrorTypeEnum. Не волнуйтесь, мы создадим их позже. Мы также установим следующие свойства для переменной password_hash:
- выбрать: ложь --При запросе пользователя не возвращайте этот столбец информации.
- имя: 'пароль' ——Установите фактическое имя столбца на «пароль», если этот параметр не установлен, TypeORM автоматически сгенерирует имя переменной в качестве имени столбца.
Создайте модуль проекта
Как создать модуль проекта и создатьпользовательский модультаким же образом. Вам также необходимо создать службу проекта и контроллер проекта.
Создать объект проекта
import {BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm';
import {UserEntity} from '../user/user.entity';
@Entity({name: 'projects'})
export class ProjectEntity extends BaseEntity{
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@ManyToOne(type => UserEntity)
user: UserEntity;
}
Теперь нам нужно сообщить TypeORM об этих объектах, а также установить параметры конфигурации, чтобы разрешить TypeORM подключаться к базе данных sqlite.
Добавьте следующий код в AppModule:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {TypeOrmModule} from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { ProjectModule } from './project/project.module';
import {UserEntity} from './user/user.entity';
import {ProjectEntity} from './project/project.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: `${process.cwd()}/tutorial.sqlite`,
entities: [UserEntity, ProjectEntity],
synchronize: true,
// logging: 'all'
}),
UserModule,
ProjectModule,
],
controllers: [AppController],
providers: [ AppService ],
})
export class AppModule {}
loggingотносится к журналу, мы прокомментировали его, но вы можетеtypeorm.io/#/loggingчтобы узнать больше информации.
пользовательский модульТеперь это должно выглядеть так:
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import {TypeOrmModule} from '@nestjs/typeorm';
import {UserEntity} from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [UserController],
providers: [UserService]
})
export class UserModule {}
модуль проектаТеперь это должно выглядеть так:
import { Module } from '@nestjs/common';
import { ProjectController } from './project.controller';
import { ProjectService } from './project.service';
import {TypeOrmModule} from '@nestjs/typeorm';
import {ProjectEntity} from './project.entity';
@Module({
imports: [TypeOrmModule.forFeature([ProjectEntity])],
controllers: [ProjectController],
providers: [ProjectService]
})
export class ProjectModule {}
Установить глобальную обработку исключений
существуетsrc/Создайте общий каталог под каталогом.Под общим каталогом мы создадим два каталога: ошибка и фильтры. (См. скриншот структуры проекта в начале статьи)
Каталог ошибок
Как показано ниже, создайтеAppErrorTypeEnum.tsдокумент.
export const enum AppErrorTypeEnum {
USER_NOT_FOUND,
USER_EXISTS,
NOT_IN_SESSION,
NO_USERS_IN_DB
}
Мы создадим переменную типа перечисления, которая не является объектом, но генерирует простую карту отношения var->number.Если нет необходимости находить представление перечисления в виде строки, производительность будет лучше, если вы выберете создать перечисление const high.
Как показано ниже, создайтеIErrorMessage.tsдокумент.
import {AppErrorTypeEnum} from './AppErrorTypeEnum';
import {HttpStatus} from '@nestjs/common';
export interface IErrorMessage {
type: AppErrorTypeEnum;
httpStatus: HttpStatus;
errorMessage: string;
userMessage: string;
}
Это будет структура JSON, возвращаемая пользователю.
Наконец, как показано ниже, создайтеAppError.tsдокумент.
import {AppErrorTypeEnum} from './AppErrorTypeEnum';
import {IErrorMessage} from './IErrorMessage';
import {HttpStatus} from '@nestjs/common';
export class AppError extends Error {
public errorCode: AppErrorTypeEnum;
public httpStatus: number;
public errorMessage: string;
public userMessage: string;
constructor(errorCode: AppErrorTypeEnum) {
super();
const errorMessageConfig: IErrorMessage = this.getError(errorCode);
if (!errorMessageConfig) throw new Error('Unable to find message code error.');
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.httpStatus = errorMessageConfig.httpStatus;
this.errorCode = errorCode;
this.errorMessage = errorMessageConfig.errorMessage;
this.userMessage = errorMessageConfig.userMessage;
}
private getError(errorCode: AppErrorTypeEnum): IErrorMessage {
let res: IErrorMessage;
switch (errorCode) {
case AppErrorTypeEnum.USER_NOT_FOUND:
res = {
type: AppErrorTypeEnum.USER_NOT_FOUND,
httpStatus: HttpStatus.NOT_FOUND,
errorMessage: 'User not found',
userMessage: 'Unable to find the user with the provided information.'
};
break;
case AppErrorTypeEnum.USER_EXISTS:
res = {
type: AppErrorTypeEnum.USER_EXISTS,
httpStatus: HttpStatus.UNPROCESSABLE_ENTITY,
errorMessage: 'User exisists',
userMessage: 'Username exists'
};
break;
case AppErrorTypeEnum.NOT_IN_SESSION:
res = {
type: AppErrorTypeEnum.NOT_IN_SESSION,
httpStatus: HttpStatus.UNAUTHORIZED,
errorMessage: 'No Session',
userMessage: 'Session Expired'
};
break;
case AppErrorTypeEnum.NO_USERS_IN_DB:
res = {
type: AppErrorTypeEnum.NO_USERS_IN_DB,
httpStatus: HttpStatus.NOT_FOUND,
errorMessage: 'No Users exits in the database',
userMessage: 'No Users. Create some.'
};
break;
}
return res;
}
}
Этот код говорит, что когда мы выдаем ошибку в любом месте кода, глобальный обработчик исключений поймает ее и вернет объект, структура которого соответствует IErrorMessage.
каталог фильтров
Как показано ниже, создайтеDispatchError.tsдокумент.
import {ArgumentsHost, Catch, ExceptionFilter, HttpStatus, UnauthorizedException} from '@nestjs/common';
import {AppError} from '../error/AppError';
@Catch()
export class DispatchError implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost): any {
const ctx = host.switchToHttp();
const res = ctx.getResponse();
if (exception instanceof AppError) {
return res.status(exception.httpStatus).json({
errorCode: exception.errorCode,
errorMsg: exception.errorMessage,
usrMsg: exception.userMessage,
httpCode: exception.httpStatus
});
} else if (exception instanceof UnauthorizedException) {
console.log(exception.message);
console.error(exception.stack);
return res.status(HttpStatus.UNAUTHORIZED).json(exception.message);
} else if (exception.status === 403) {
return res.status(HttpStatus.FORBIDDEN).json(exception.message);
}
else {
console.error(exception.message);
console.error(exception.stack);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send();
}
}
}
Вы можете реализовать этот класс любым удобным для вас способом, приведенный выше код является лишь небольшим примером.
Теперь все, что нам нужно сделать, это заставить приложение использовать этот фильтр, что очень просто. в нашемmain.tsДобавьте следующее:
app.useGlobalFilters(new DispatchError());
Теперь вашmain.tsСодержимое файла примерно такое:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {DocumentBuilder, SwaggerModule} from '@nestjs/swagger';
import {DispatchError} from './common/filters/DispatchError';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new DispatchError());
const options = new DocumentBuilder()
.setTitle('User Session Tutorial')
.setDescription('Basic Auth and session management')
.setVersion('1.0')
.addTag('nestjs')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
создать и получить пользователя
Хорошо, теперь мы готовы добавить логику для создания и получения пользователя. Давайте следовать стилю сервисов в Spring Boot. Наш пользовательский сервис будет реализован в IUserService. В папке пользователя создайтеIUserService.tsдокумент. В то же время нам также необходимо определить модель, которая будет использоваться в запросе на создание пользователя. Создайтеuser/models/CreateUserDto.tsдокумент.
import {ApiModelProperty} from '@nestjs/swagger';
export class CreateUserDto {
@ApiModelProperty()
readonly firstName: string;
@ApiModelProperty()
readonly lastName: string;
@ApiModelProperty()
readonly username: string;
@ApiModelProperty()
readonly password: string;
}
Основная функция этого класса — сообщить Swagger, какую структуру данных он должен отправлять.
вот нашIUserService.ts.
import {CreateUserDto} from './models/CreateUserDto';
import {UserEntity} from './user.entity';
import {ProjectEntity} from '../project/project.entity';
export interface IUserService {
findAll(): Promise<UserEntity[]>;
createUser(user: CreateUserDto): Promise<UserEntity>;
getProjectsForUser(user: UserEntity): Promise<ProjectEntity[]>;
}
вот нашuser.service.ts.
import { Injectable } from '@nestjs/common';
import {UserEntity} from './user.entity';
import {IUserService} from './IUserService';
import {CreateUserDto} from './models/CreateUserDto';
import {ProjectEntity} from '../project/project.entity';
@Injectable()
export class UserService implements IUserService{
public async findAll(): Promise<UserEntity[]> {
return await UserEntity.findAll();
}
public async createUser(user: CreateUserDto): Promise<UserEntity> {
return await UserEntity.createUser(user);
}
public async getProjectsForUser(user: UserEntity): Promise<ProjectEntity[]> {
return undefined;
}
}
Ну наконец тоuser.controller.ts.
import {Body, Controller, Get, HttpStatus, Post, Req, Res, Session} from '@nestjs/common';
import {UserService} from './user.service';
import {ApiBearerAuth, ApiOperation, ApiResponse} from '@nestjs/swagger';
import {UserEntity} from './user.entity';
import {CreateUserDto} from './models/CreateUserDto';
import {Request, Response} from 'express';
@Controller('user')
export class UserController {
constructor(private readonly usersService: UserService) {}
@Get('')
@ApiOperation({title: 'Get List of All Users'})
@ApiResponse({ status: 200, description: 'User Found.'})
@ApiResponse({ status: 404, description: 'No Users found.'})
public async getAllUsers(@Req() req: Request, @Res() res, @Session() session) {
const users: UserEntity[] = await this.usersService.findAll();
return res
.status(HttpStatus.OK)
.send(users);
}
@Post('')
@ApiOperation({title: 'Create User'})
public async create(@Body() createUser: CreateUserDto, @Res() res) {
await this.usersService.createUser(createUser);
return res.status(HttpStatus.CREATED).send();
}
}
Элегантность контроллера заключается в том, что он просто возвращает пользователю результат успеха, нам не нужно обрабатывать какие-либо исключения ошибок, потому что они обрабатываются глобальным обработчиком исключений.
Теперь, запустивnpm run startилиnpm run start:devзапустить сервер(npm run start:devбудет следить за изменениями в вашем коде и перезапускать сервер при каждом сохранении). После запуска сервера доступhttp://localhost:3000/api/#/.
Если все прошло хорошо, вы должны увидеть интерфейс Swagger и некоторые интерфейсы API. читатьучебник по sqliteИ выберите инструмент sqlite, который вы считаете подходящим (в браузере Firefox есть расширение для sqlite), чтобы подтвердить правильность схемы данных. Когда в базе данных нет пользователей, попробуйте получить всех пользователей, он должен вернуть код состояния 404 и сообщение, содержащее userMessage, errorMessage и т. д.AppError.tsинформация определена в) JSON. Теперь создайте пользователя и выполните, чтобы получить всех пользователей. Если все работает нормально, то идем дальше и создаемАвторизоватьсяинтерфейс API. Если у вас есть какие-либо вопросы, пожалуйста, оставьте вопрос в разделе комментариев.
Реализовать аутентификацию
существуетuser.controller.tsДобавьте следующий код после файла.
@Post('login')
@ApiOperation({title: 'Authenticate'})
@ApiBearerAuth()
public async login(@Req() req: Request, @Res() res: Response, @Session() session) {
return res.status(HttpStatus.OK).send();
}
@ApiBearerAuth()Аннотация предназначена для того, чтобы Swagger знал, что с этим запросом мы хотим отправить Basic Auth в заголовке. Тем не менее, мы также должны добавить некоторый код вmain.tsсередина.
const options = new DocumentBuilder()
.setTitle('User Session Tutorial')
.setDescription('Basic Auth and session management')
.setVersion('1.0')
.addTag('nestjs')
.addBearerAuth('Authorization', 'header')
.build();
Теперь, если мы перезапустим сервер, мы увидим небольшой значок замка рядом с интерфейсом API. Но в этом интерфейсе сейчас ничего нет, так что давайте добавим в него немного логики. Когда я пишу этот учебник, я не думаю, что документация достаточно хороша для правильной реализации такого рода функций, я следовалОфициальная документация NestJSдобиться, но столкнулся со следующимпроблема. Однако я нашел@nestjs/passportЭта библиотека, я могу использовать ее с:
Прежде чем разрабатывать логику аутентификации, нам нужно добавить следующее:main.tsсередина.
* import * as passport from 'passport';
import * as session from 'express-session'
app.use(session({
secret: 'secret-key',
name: 'sess-tutorial',
resave: false,
saveUninitialized: false
}))
app.use(passport.initialize());
app.use(passport.session());
Создайте модуль авторизации
воплощать в жизньnest g mo authиnest g s auth, который создаст модуль авторизации сauthсодержание. Как и раньше, если auth.service был сгенерирован вне каталога auth, просто переместите его. Официальная документация NestJS говорит, что его нужно использовать здесь@UseGuards(AuthGuard('носитель'))Но из-за проблемы, о которой я только что упомянул, я сам внедрил AuthGuard и могу сам авторизовать пользователей. Далее нам также нужно реализовать нашу «стратегию паса». Создайтеsrc/auth/AppAuthGuard.tsдокумент.
import {CanActivate, ExecutionContext, UnauthorizedException} from '@nestjs/common';
import * as passport from 'passport';
export class AppAuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const options = { ...defaultOptions };
const httpContext = context.switchToHttp();
const [request, response] = [
httpContext.getRequest(),
httpContext.getResponse()
];
const passportFn = createPassportContext(request, response);
const user = await passportFn(
'bearer',
options
);
if (user) {
request.login(user, (res) => {});
}
return true;
}
}
const createPassportContext = (request, response) => (type, options) =>
new Promise((resolve, reject) =>
passport.authenticate(type, options, (err, user, info) => {
try {
return resolve(options.callback(err, user, info));
} catch (err) {
reject(err);
}
})(request, response, resolve)
);
const defaultOptions = {
session: true,
property: 'user',
callback: (err, user, info) => {
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
};
Создайтеsrc/auth/http.strategy.tsдокумент.
import {Injectable} from '@nestjs/common';
import {PassportStrategy} from '@nestjs/passport';
import { Strategy } from 'passport-http-bearer';
@Injectable()
export class HttpStrategy extends PassportStrategy(Strategy) {
async validate(token: any, done: Function) {
done(null, {user: 'test'});
}
}
- token—— В заголовке запроса мы получим токен, обычно называемый «токен», и его формат следующий:«Base64encode носителя ('somestring')».
- done(null, {user: test})- Сохранить объект во втором параметре сеанса. Сейчас мы временно сохраним поддельный объект, который позже заменим пользовательским объектом, полученным из базы данных.
возобновитьAuthModule.tsдокумент.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import {HttpStrategy} from './http.strategy';
import {AppAuthGuard} from './AppAuthGuard';
@Module({
providers: [AuthService, HttpStrategy, AppAuthGuard]
})
export class AuthModule {}
Теперь запустим наш сервер.
Лучший способ протестировать наш проект — зайти в Swagger API в своем браузере, щелкнуть значок замка и ввести «Bearer test, а затем нажмите «Авторизовать».Инструменты разработчика Chromeпереключить наApplicationнажмите на левой панели,Cookies->http://localhost:3000. нажмите сейчасPOST /login«Выполнить» интерфейса для выдачи запроса. Мы ожидаем увидеть файл с именем "sess-tutorial"cookie. Но пока ничего не видим. Что не так? Если приглядетьсядокументы на паспорт, вы обнаружите, что нам также нужно добавить следующее к объекту паспорта.
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function (err, user) {
done(err, user);
});
});
Документация говорит,@nestjs/passportесть имяPassportSerializerабстрактный класс. Почему это должен быть абстрактный класс? Давайте сначала попробуем, сначала реализуем абстрактный класс как конкретный класс и добавим@Injectable()аннотации, а затем для нашегоauth.module.ts.использовать.
Как показано ниже, создайтеsrc/auth/cookie-serializer.tsдокумент.
import {PassportSerializer} from '@nestjs/passport/dist/passport.serializer';
import {Injectable} from '@nestjs/common';
@Injectable()
export class CookieSerializer extends PassportSerializer {
serializeUser(user: any, done: Function): any {
done(null, user);
}
deserializeUser(payload: any, done: Function): any {
done(null, payload);
}
}
AuthModule
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import {HttpStrategy} from './http.strategy';
import {AppAuthGuard} from './AppAuthGuard';
import {CookieSerializer} from './cookie-serializer';
@Module({
providers: [AuthService, HttpStrategy, AppAuthGuard, CookieSerializer]
})
export class AuthModule {}
Теперь запустите наш сервер и запросите его с заголовком Basic Auth.POST /loginинтерфейс, теперь мы должны увидеть файл cookie в Chrome DevTools. Только что мы столкнулись с небольшой проблемой, но, прочитав документацию по разработке и@nestjs/passportdocs мы быстро нашли ответ.
Теперь нам нужно добавить логику для аутентификации пользователя по записи в базе данных и гарантировать, что запросы маршрутизации будут выполняться только в том случае, если пользователь вошел в систему.
Добавьте следующую функцию вUserEntity.tsсередина.
public static async authenticateUser(user: {username: string, password: string}): Promise<UserEntity> {
let u: UserEntity;
u = await UserEntity.findOne({
select: ['id', 'username', 'password_hash'],
where: { username: user.username}
});
const passHash = crypto.createHmac('sha256', user.password).digest('hex');
if (u.password_hash === passHash) {
delete u.password_hash;
return u;
}
}
и обновитьAuthService.ts.
import { Injectable } from '@nestjs/common';
import {UserEntity} from '../user/user.entity';
@Injectable()
export class AuthService {
async validateUser(user: {username: string, password: string}): Promise<any> {
return await UserEntity.authenticateUser(user);
}
}
Затем измените нашhttp.strategy.ts.
import {Injectable, UnauthorizedException} from '@nestjs/common';
import {PassportStrategy} from '@nestjs/passport';
import { Strategy } from 'passport-http-bearer';
import {AuthService} from './auth.service';
@Injectable()
export class HttpStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(token: any, done: Function) {
let authObject: {username: string, password: string} = null;
const decoded = Buffer.from(token, 'base64').toString();
try {
authObject = JSON.parse(decoded);
const user = await this.authService.validateUser(authObject);
if (!user) {
return done(new UnauthorizedException(), false);
}
done(null, user);
} catch (e) {
return done(new UnauthorizedException(), false);
}
}
}
Откройте сейчас бесплатносайт с шифрованием base64, следующие поля JSON зашифрованы и будут отправлены в Swagger.
{
"username" : "johnny",
"password": "1234"
}
Теперь вернитесь к Swagger, просто нажмите справаAuthorizeВведите во всплывающем окне ввода"Носитель ew0KICAidXNlcm5hbWUiIDogImpvaG5ueSIsDQogICJwYXNzd29yZCI6ICIxMjM0Ig0KfQ==". Строка после Bearer — это строка JSON, только что зашифрованная выше, которая будет вUserEntity.tsизauthenticateUserфункция декодируется и сопоставляется. выполнить сейчасPOST /login, вы должны увидеть файл cookie в инструментах разработчика Chrome (если ваш пользователь находится в базе данных с именем пользователя «jonny» и паролем «1234»).
Давайте создадим маршрут, который будет использоваться для создания элемента для текущего пользователя, вошедшего в систему, но перед этим нам понадобится «сохранитель сеанса», который защитит наш маршрут, и если в сеансе нет пользователя, он выдаст AppError.
Защита маршрутизации от несанкционированного доступа
Создайтеsrc/auth/SessionGuard.tsдокумент.
import {CanActivate, ExecutionContext} from '@nestjs/common';
import {AppError} from '../common/error/AppError';
import {AppErrorTypeEnum} from '../common/error/AppErrorTypeEnum';
export class SessionGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const httpContext = context.switchToHttp();
const request = httpContext.getRequest();
try {
if (request.session.passport.user)
return true;
} catch (e) {
throw new AppError(AppErrorTypeEnum.NOT_IN_SESSION);
}
}
}
Мы также можем получить объект пользователя из сеанса более удобным способом. использоватьreq.session.passport.userТакой способ возможен, но недостаточно элегантен. Теперь создайтеsrc/user/user.decorator.tsдокумент.
import {createParamDecorator} from '@nestjs/common';
export const SessionUser = createParamDecorator((data, req) => {
return req.session.passport.user;
})
Далее мы кProjectEntityДобавьте в класс функцию для создания проекта для данного пользователя.
public static async createProjects(projects: CreateProjectDto[], user: UserEntity): Promise<ProjectEntity[]> {
const u: UserEntity = await UserEntity.findOne(user.id);
if (!u) throw new AppError(AppErrorTypeEnum.USER_NOT_FOUND);
const projectEntities: ProjectEntity[] = [];
projects.forEach((p: CreateProjectDto) => {
const pr: ProjectEntity = new ProjectEntity();
pr.name = p.name;
pr.description = p.description;
projectEntities.push(pr);
});
u.projects = projectEntities;
const result: ProjectEntity[] = await ProjectEntity.save(projectEntities);
await UserEntity.save([u]);
return Promise.all(result);
}
существуетProjectServiceкласс, добавьте следующее содержимое.
public async createProject(projects: CreateProjectDto[], user: UserEntity): Promise<ProjectEntity[]> {
return ProjectEntity.createProjects(projects, user);
}
обновить сноваProjectController.
import {Body, Controller, HttpStatus, Post, Res, UseGuards} from '@nestjs/common';
import {SessionGuard} from '../auth/SessionGuard';
import {SessionUser} from '../user/user.decorator';
import {UserEntity} from '../user/user.entity';
import {ApiOperation, ApiUseTags} from '@nestjs/swagger';
import {CreateProjectDto} from './models/CreateProjectDto';
import {ProjectService} from './project.service';
import {ProjectEntity} from './project.entity';
@ApiUseTags('project')
@Controller('project')
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
@Post('')
@UseGuards(SessionGuard)
@ApiOperation({title: 'Create a project for the logged in user'})
public async createProject(@Body() createProjects: CreateProjectDto[], @Res() res, @SessionUser() user: UserEntity) {
const projects: ProjectEntity[] = await this.projectService.createProject(createProjects, user);
return res.status(HttpStatus.OK).send(projects);
}
}
- @UseGuards(SessionGuard)- Если пользователь не находится в сеансе, предопределенный JSON будет возвращен в AppError ответа.
- @SessionUser()- Наш пользовательский декоратор позволяет нам легко получить из сеансаUserEntityобъект. (На самом деле нам не нужно хранить весьUserEntityобъект, мы можем сохранить идентификатор пользователя, изменив класс CookieSerializer).
В Swagger попробуйте создать проект отдельно без аутентификации пользователя и входа пользователя, чтобы увидеть разницу. Когда вы создаете проект, вы должны отправить массив элементов. (Обратите внимание, что сеанс будет потерян после перезапуска сервера). Вы также можете удалить файл cookie с помощью инструментов разработчика Chrome.
Теперь давайте добавим пользовательский функционал для получения проекта.
Получить проект для аутентифицированного пользователя
существуетProjectEntityДобавьте следующий код в:
public static async getProjects(user: UserEntity): Promise<ProjectEntity[]> {
const u: UserEntity = await UserEntity.findOne(user.id, { relations: ['projects']});
if (!u) throw new AppError(AppErrorTypeEnum.USER_NOT_FOUND);
return Promise.all(u.projects);
}
существуетProjectServiceДобавьте следующий код в:
public async getProjectsForUser(user: UserEntity): Promise<ProjectEntity[]> {
return ProjectEntity.getProjects(user);
}
существуетProjectControllerДобавьте следующий код в:
@Get('')
@UseGuards(SessionGuard)
@ApiOperation({title: 'Get Projects for User'})
public async getProjects(@Res() res, @SessionUser() user: UserEntity) {
const projects: ProjectEntity[] = await this.projectService.getProjectsForUser(user);
return res.status(HttpStatus.OK).send(projects);
}
Вот и все.
ты сможешьGitHub.com/AR Тонио/РЭШ…Ознакомьтесь с завершенным исходным кодом.
Примечание переводчика: Оригинальная статья автора была написана в 2018 г. Версия NestJS 5.0.0, сейчас NestJS обновлен до v6, поэтому он несовместим. Но официальный представитель NestJS перешел с версии 5 на версию 6.Руководство по миграции, вы можете обратиться к нему при необходимости. Точно так же другие библиотеки, упомянутые в статье, также должны обращать внимание на версию.
Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из ИнтернетаНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллекти другие поля, если вы хотите видеть больше качественных переводов, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.