Пошук уроків, статей та іншого контенту
Дізнаєтеся, як безпечно хешувати паролі, перевіряти їх і захищати облікові дані користувачів.
Пароль не потрібно зберігати в базі даних у відкритому вигляді. Якщо база даних стане доступною зловмиснику, відкриті паролі можна буде одразу використати.
Хешування перетворює пароль на рядок фіксованого формату:
МійСкладнийПароль123перетворюється, наприклад, на:
$2b$12$...Хешування має такі властивості:
його результат зберігають замість пароля;
початковий пароль не відновлюють із хешу;
той самий пароль зазвичай дає різні хеші завдяки випадковій солі;
пароль перевіряють порівнянням із хешем, а не розшифруванням.
Хешування відрізняється від шифрування. Зашифровані дані можна розшифрувати за допомогою ключа, а хеш пароля не потрібно і неможливо розшифровувати.
Для хешування паролів у NestJS часто використовують пакет bcrypt.
Встановіть його:
npm install bcrypt
npm install --save-dev @types/bcryptbcrypt автоматично:
генерує випадкову сіль;
додає її до хешу;
виконує обчислення, спеціально зроблені повільними;
зберігає параметри хешування всередині результату.
Завдяки цьому сіль не потрібно зберігати в окремому полі бази даних.
Створимо окремий сервіс, щоб логіка хешування не дублювалася в різних частинах застосунку.
// src/auth/password.service.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
@Injectable()
export class PasswordService {
// Кількість раундів bcrypt. Більше значення підвищує захист,
// але також збільшує час обчислення.
private readonly saltRounds = 12;
async hash(password: string): Promise<string> {
return bcrypt.hash(password, this.saltRounds);
}
async compare(
password: string,
passwordHash: string,
): Promise<boolean> {
return bcrypt.compare(password, passwordHash);
}
}Зареєструйте сервіс у модулі:
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PasswordService } from './password.service';
@Module({
providers: [PasswordService],
exports: [PasswordService],
})
export class AuthModule {}Тепер PasswordService можна використовувати в сервісах, які реєструють користувачів або виконують вхід.
Під час реєстрації потрібно хешувати пароль до збереження користувача в базу даних.
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { PasswordService } from '../auth/password.service';
interface User {
id: number;
email: string;
passwordHash: string;
}
@Injectable()
export class UsersService {
private nextId = 1;
private readonly users: User[] = [];
constructor(
private readonly passwordService: PasswordService,
) {}
async createUser(
email: string,
password: string,
): Promise<Omit<User, 'passwordHash'>> {
const passwordHash = await this.passwordService.hash(password);
const user: User = {
id: this.nextId++,
email,
passwordHash,
};
this.users.push(user);
// Не повертаємо хеш пароля клієнту.
const { passwordHash: _, ...safeUser } = user;
return safeUser;
}
async findByEmail(email: string): Promise<User | undefined> {
return this.users.find((user) => user.email === email);
}
}У базі даних потрібно зберігати значення passwordHash, а не поле password.
Наприклад, запис користувача може мати такий вигляд:
email: user@example.com
passwordHash: $2b$12$...Відкритий пароль не повинен потрапляти:
у базу даних;
у журнали застосунку;
у відповіді API;
у повідомлення про помилки;
у звичайні об’єкти, які передаються між сервісами без потреби.
Під час входу користувач надсилає адресу електронної пошти та пароль. Сервер знаходить користувача за електронною поштою і перевіряє пароль за допомогою bcrypt.compare.
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PasswordService } from './password.service';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly passwordService: PasswordService,
) {}
async validateUser(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('Неправильні облікові дані');
}
const isPasswordValid = await this.passwordService.compare(
password,
user.passwordHash,
);
if (!isPasswordValid) {
throw new UnauthorizedException('Неправильні облікові дані');
}
// Хеш пароля не повинен залишати межі сервісу автентифікації.
const { passwordHash: _, ...safeUser } = user;
return safeUser;
}
}Метод compare сам читає сіль і параметри збереженого хешу. Тому не потрібно окремо діставати сіль або створювати нову сіль під час перевірки.
Сервіс автентифікації може використовуватися в контролері для обробки входу:
// src/auth/auth.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
class LoginDto {
email: string;
password: string;
}
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(@Body() body: LoginDto) {
const user = await this.authService.validateUser(
body.email,
body.password,
);
return {
user,
};
}
}У реальному застосунку після успішної перевірки зазвичай створюють сесію або токен доступу. Однак незалежно від способу подальшої автентифікації пароль перевіряють саме через хеш.
Методи bcrypt.hash і bcrypt.compare мають асинхронні версії. У NestJS потрібно використовувати await:
const passwordHash = await bcrypt.hash(password, 12);
const isValid = await bcrypt.compare(password, passwordHash);Асинхронний підхід не блокує обробку інших запитів під час обчислення хешу. Не варто використовувати синхронні варіанти hashSync і compareSync у серверному коді, особливо під навантаженням.
Другий аргумент bcrypt.hash визначає кількість раундів обчислення:
const passwordHash = await bcrypt.hash(password, 12);Зі збільшенням кількості раундів:
хеш стає складнішим для перебору;
хешування та перевірка займають більше часу;
зростає навантаження на сервер.
Значення потрібно підбирати за допомогою вимірювань у середовищі, де працюватиме застосунок. Не слід бездумно використовувати дуже велике число, оскільки це може сповільнити реєстрацію та вхід користувачів.
Неправильно:
const user = {
email,
password,
};Правильно:
const passwordHash = await passwordService.hash(password);
const user = {
email,
passwordHash,
};Неправильно порівнювати новий хеш із збереженим рядком:
const newHash = await bcrypt.hash(password, 12);
const isValid = newHash === savedHash;Через випадкову сіль два хеші одного пароля зазвичай будуть різними.
Правильно:
const isValid = await bcrypt.compare(password, savedHash);Навіть хеш не потрібно повертати у відповіді API. Він є конфіденційним значенням, яке може допомогти зловмиснику під час атак на базу даних.
Не додавайте пароль до console.log, логів HTTP-запитів або повідомлень про помилки:
// Неправильно:
// console.log('Пароль користувача:', password);Під час входу краще повертати загальне повідомлення для випадків, коли користувача не знайдено або пароль неправильний:
throw new UnauthorizedException('Неправильні облікові дані');Так відповідь не повідомляє, чи існує вказана електронна адреса.
Паролі потрібно хешувати до збереження в базі даних.
Для хешування паролів у NestJS можна використовувати bcrypt.
Перевіряти пароль потрібно через bcrypt.compare, а не повторним хешуванням.
Сіль генерується та зберігається всередині хешу автоматично.
Хеш пароля не можна повертати клієнту або записувати в логи.
Для серверного коду слід використовувати асинхронні методи hash і compare.
Помилки входу краще повертати одним загальним повідомленням.