088 Работа JWT
JWT (JSON Web Token) - это объект, который обеспечивает безопасную передачу данных в шифрованном виде.
Токен разбит на 3 части:
HEADER
- хранит в себе тип и алгоритм шифрования.PAYLOAD
- сами передаваемые данные на сервер.SIGNATURE
- подпись, по которой идёт проверка. Так же она хранит секрет, по которому будет происходить дешифровка данных на сервере.
089 Создание токена
Устанавливаем АПИ модуль по работе с токенами
npm i jsonwebtoken
npm i -D @types/jsonwebtoken
Метод sign
реализует само тело PAYLOAD
Конкретно добавление свойства iat
позволяет нам избежать утечки токенов и каждый раз они будут создаваться новыми. Так же даже если наш токен утечёт, то злоумышленник сможет им пользоваться только определённый промежуток времени, который мы зададим самостоятельно. Так как у злоумышленника не будет refresh-token
, то он не сможет его обновить, а мы на сервере сможем и по нему будем уже давать доступ пользователю.
Добавляем секрет в наше окружение
.env
SALT=10
SECRET="MYAPP"
И далее реализуем получение jwt
. Через ConfigService
получаем из окружение секрет. В signJWT()
реализуем генерацию токена через метод входа sign()
user.controlle.ts
import { sign } from 'jsonwebtoken';
@injectable()
export class UserController extends BaseController implements IUserController {
constructor(
@inject(TYPES.ILogger) private loggerService: ILogger,
@inject(TYPES.UserService) private userService: IUserService,
// добавляем конфиг сервис, который создан через синглтон
@inject(TYPES.ConfigService) private configService: IConfigService,
) {
/// CODE ...
}
// реализаця метода логина пользователя
async login(
req: Request<{}, {}, UserLoginDto>,
res: Response,
next: NextFunction,
): Promise<void> {
// получаем ответ от валидатора на основе переданного в него пользователя из тела запроса
const result = await this.userService.validateUser(req.body);
// если результата нет, то выведем ошибку
if (!result) {
return next(new HTTPError(401, 'ошибка авторизации', 'login'));
}
// получаем наш jwt
const jwt = await this.signJWT(req.body.email, this.configService.get('SECRET'));
// если всё нормально, то отправляем ответ
this.ok(res, { jwt });
}
/// CODE ...
private async signJWT(email: string, secret: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
// принимает в себя payload
sign(
// payload
{
email,
// обязательно нужно добавить дату создания
iat: Math.floor(Date.now() / 1000),
},
// второй параметр - секрет
secret,
// опции - HEAD
{
algorithm: 'HS256',
},
// дальше идёт функция, которая принимает токен или ошибку
(err, token) => {
if (err) {
reject(err);
}
resolve(token as string);
},
);
});
}
}
И такой ответ мы получаем от сервера
На сайте JWT можно посмотреть раскодированный токен, если вставить его и секрет
090 Middleware для проверки jwt
Сразу нужно отметить, что все действия по работе с JWT можно оптимизировать, используя библиотеку к Express - Passport
Нам далее пригодиться переписать реквест таким образом, чтобы он имел в себе так же и значение пользователя. Интерфейсы имеют свойство объединяться в TS, поэтому декларируем пространство имён и дополняем в нём интерфейс запроса, чтобы в нём было свойство пользователя - таким образом мы обогащаем наш запрос
types > custom.d.ts
declare namespace Express {
export interface Request {
user: string;
}
}
Тут уже реализуем интерфейс аутентификации пользователя
примечение: тут payload
будет первое время возвращать ошибку на свойство email
ровно до того момента, пока не сделаем spec
в тестах, так как там будет указана почта
common > auth.middleware.ts
import { IMiddleware } from './middleware.interface';
import { NextFunction, Response, Request } from 'express';
import { verify } from 'jsonwebtoken';
// это посредник для реализации аутентификации в системе
export class AuthMiddleware implements IMiddleware {
constructor(private secret: string) {}
execute(req: Request, res: Response, next: NextFunction): void {
// если в ответе есть данные авторизации
if (req.headers.authorization) {
// дальше пойдёт проверка токена
// verify проверяет соответствие токена секрету
// эта функция асинхронна
verify(
// Bearer JWT - так выглядит хранимая информация
// нам нужно её отделить и получить только один токен без первого слова
req.headers.authorization.split(' ')[1],
// сюда мы передаём секрет
this.secret,
// функция, которая выполнится по результатам проверки
(err, payload) => {
// если выскочила ошибка
if (err) {
// то ничего не делаем
next();
// если получили данные
} else if (payload) {
// то обогащаем ответ дополнительными данными о пользователе
req.user = payload.email;
next();
}
},
);
} else {
next();
}
}
}
Далее нам нужно реализовать использование аутентификации через основной метод использования посредников
app.ts
useMiddleware(): void {
this.app.use(json());
// инициализируем аутентификацию и передаём в неё секрет
const authMiddleware = new AuthMiddleware(this.configService.get('SECRET'));
// биндим контекст запуска
this.app.use(authMiddleware.execute.bind(authMiddleware));
}
Добавим в контроллер пользователя метод info
, который будет возвращать данные о пользователе
users.controller.interface.ts
import { NextFunction, Request, Response } from 'express';
export interface IUserController {
login: (req: Request, res: Response, next: NextFunction) => void;
register: (req: Request, res: Response, next: NextFunction) => void;
// будет возвращать информацию о пользователе, который к нам пришёл
info: (req: Request, res: Response, next: NextFunction) => void;
}
И теперь тут инициализируем middleware
и создём метод получения информации о пользователе
users.controller.ts
constructor(
@inject(TYPES.ILogger) private loggerService: ILogger,
@inject(TYPES.UserService) private userService: IUserService,
@inject(TYPES.ConfigService) private configService: IConfigService,
) {
super(loggerService);
this.bindRoutes([
{
path: '/register',
method: 'post',
func: this.register,
middlewares: [new ValidateMiddleware(UserRegisterDto)],
},
{
path: '/login',
method: 'post',
func: this.login,
middlewares: [new ValidateMiddleware(UserLoginDto)],
},
// Конкретно тут обрабатывается запрос получения информации о пользователе
{
path: '/info',
method: 'get',
func: this.info,
middlewares: [],
},
]);
}
async info({ user }: Request, res: Response, next: NextFunction): Promise<void> {
this.ok(res, { email: user });
}
Так же в нодмон нужно добавить флаг --files
, который позволит подтянуть кастомные типы
nodemon.json
{
"watch": [
"src"
],
"ext": "ts,json",
"ignore": [
"src/**/*.spec.ts"
],
"exec": "ts-node --files ./src/main.ts"
}
И теперь можно увидеть, что сервер возвращает данные о пользователе, передав в хедер запроса токен пользователя
091 Упражнение - Guard авторизации
Далее нам нужно реализовать guard
, который будет ограничивать вход пользователей в приложение, если они не будут соответствовать определённым требованиям
common > auth.guard.ts
import { IMiddleware } from './middleware.interface';
import { NextFunction, Request, Response } from 'express';
export class AuthGuard implements IMiddleware {
execute(req: Request, res: Response, next: NextFunction): void {
// если в ответе будет находиться пользователь
if (req.user) {
// завершаем исполнение функции и продолжаем выполнение посредников
return next();
}
// если нет тела в ответе, то отправляем ошибку
res.status(401).send({ error: 'Вы не авторизован' });
}
}
Далее для get
запроса добавим гуарда
users.controller.ts
constructor(
@inject(TYPES.ILogger) private loggerService: ILogger,
@inject(TYPES.UserService) private userService: IUserService,
@inject(TYPES.ConfigService) private configService: IConfigService,
) {
super(loggerService);
this.bindRoutes([
{
path: '/register',
method: 'post',
func: this.register,
middlewares: [new ValidateMiddleware(UserRegisterDto)],
},
{
path: '/login',
method: 'post',
func: this.login,
middlewares: [new ValidateMiddleware(UserLoginDto)],
},
{
path: '/info',
method: 'get',
func: this.info,
middlewares: [new AuthGuard()],
},
]);
}
И мы получаем такой результат при запросе, так как мы не обходим гуарда
Уже тут нужно будет реализовать метод получения данных, отправляя запрос в репозиторий (нельзя нарушать слоёную архитектуру и отправлять запрос напрямую в базу)
user.service.interface.ts
export interface IUserService {
createUser: (dto: UserRegisterDto) => Promise<UserModel | null>;
validateUser: (dto: UserLoginDto) => Promise<boolean>;
// добавляем метод получения информации о пользователе
getUserInfo: (email: string) => Promise<UserModel | null>;
}
user.service.ts
async info({ user }: Request, res: Response, next: NextFunction): Promise<void> {
// получаем информацию о пользователе
const userInfo = await this.userService.getUserInfo(user);
this.ok(res, { email: userInfo?.email, id: userInfo?.id });
}
async getUserInfo(email: string): Promise<UserModel | null> {
return this.usersRepository.find(email);
}
И теперь мы можем увидеть то, что мы прочитали непосредственно из базы - почту и id пользователя