用NestJS和Prisma: Authentication构建一个REST API
欢迎学习本系列的第五个教程,了解如何使用 NestJS、Prisma 和 PostgreSQL 构建 REST API! 在本教程中,您将学习如何在 NestJS REST API 中实现 JWT 身份验证。
介绍
在上一章的NestJS REST API系列教程中,您已经学习了如何处理关系数据。您成功创建了一个模型,并在User与Article模型之间建立了一对多的关系。此外,您还实现了这些模型的CRUD(创建、读取、更新、删除)终端节点。
在本章中,我们将深入探讨如何使用 Passport 包为API添加身份验证功能:
- 首先,您将使用名为 Passport 的库实现基于 JSON Web 令牌 (JWT) 的身份验证。
- 接下来,您将通过使用 bcrypt 库对存储在数据库中的密码进行哈希处理来保护这些密码。
请注意,本教程将基于上一章所构建的API进行扩展和修改。
开发环境
为了顺利学习本教程,您需要满足以下条件:
- 安装Node.js。
- 安装 Docker 和 Docker Compose。如果您使用的是 Linux,请确保您的 Docker 版本为 20.10.0 或更高版本。您可以通过在终端中运行来检查您的 Docker 版本。
- (可选)安装 Prisma VS Code 扩展。该扩展能为Prisma提供出色的IntelliSense和语法高亮功能。
- 可以选择访问 Unix shell(如 Linux 和 macOS 中的终端/shell)来运行本系列中提供的命令。
如果您没有Unix shell(例如,您使用的是Windows计算机),您仍然可以继续学习,但可能需要根据您的计算机环境对shell命令进行适当的修改。
克隆存储库
本教程的起点承接本系列第2章的结束部分,那里提供了一个使用NestJS构建的基础REST API。
本教程的起始点是位于end-validation
分支上。要着手进行,请克隆存储库并检出该分支:
git clone -b end-relational-data git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git
现在,执行以下操作以开始使用:
- 导航到克隆的目录:
cd blog-backend-rest-api-nestjs-prisma
- 安装依赖项:
npm install
- 使用 Docker 启动 PostgreSQL 数据库:
docker-compose up -d
- 应用数据库迁移:
npx prisma migrate dev
- 启动项目:
npm run start:dev
注意:步骤 4 还将生成 Prisma Client 并设定数据库种子。
现在,您应该能够在以下位置访问 API 文档:http://localhost:3000/api/.
项目结构和文件
您克隆的存储库应具有以下结构:
median
├── node_modules
├── prisma
│ ├── migrations
│ ├── schema.prisma
│ └── seed.ts
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── articles
│ ├── users
│ └── prisma
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── README.md
├── .env
├── docker-compose.yml
├── nest-cli.json
├── package-lock.json
├── package.json
├── tsconfig.build.json
└── tsconfig.json
注意:此文件夹还附带一个名为
test
的目录,但本教程并不涵盖测试内容。然而,如果您对使用Prisma进行应用程序测试的最佳实践感兴趣,建议您查阅本教程系列的另一篇指南:使用Prisma进行测试的终极指南。
此存储库中的关键文件和目录概述如下:
src
目录:包含应用程序的源代码。具体包括三个模块:app
模块(位于src
根目录下):作为应用程序的入口点,负责启动Web服务器。prisma
模块:包含Prisma Client,这是您与数据库交互的接口。articles
和users
模块:分别定义了路由端点及其相关的业务逻辑。
prisma
文件夹:schema.prisma
文件:定义了数据库的架构。migrations
目录:记录了数据库的迁移历史。
- 其他重要文件:
seed.ts
文件:包含一个脚本,用于使用虚拟数据为开发数据库设定初始值。docker-compose.yml
文件:定义了PostgreSQL数据库的Docker映像。.env
文件:包含了PostgreSQL数据库的连接字符串。
请注意,如需了解这些组件的更多详细信息,请参阅本教程系列的第一章。
在 REST API 中实施身份验证
在本节中,您将为 REST API 实现大部分身份验证逻辑。在本节结束时,以下端点将受到身份验证保护🔒:
GET /users
GET /users/:id
PATCH /users/:id
DELETE /users/:id
Web 上使用的身份验证主要有两种类型:基于会话的身份验证和基于令牌的身份验证。在本教程中,您将使用 JSON Web 令牌 (JWT) 实现基于令牌的身份验证。
首先,在您的应用程序中创建一个新模块。运行以下命令以生成新模块。
npx nest generate resource
您将收到一些 CLI 提示,请相应地回答问题:
- 请问您希望为这个资源使用什么名称(请使用复数形式,例如“users”)?authentications
- 您使用的传输层是什么? REST API
- 您是否需要生成CRUD的入口点? 不
现在,您应该在项目的目录中找到一个名为auth
的新模块,它位于src/auth
路径下。
安装和配置passport
passport
是Node.js应用程序中常用的一个身份验证库。它配置灵活,并支持多种身份验证策略。passport
旨在与构建NestJS应用的Express
Web框架协同工作。NestJS提供了与@nestjs/passport
的第一方集成,这使得在NestJS应用程序中使用passport
变得十分简便。
首先安装以下软件包:
npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
现在您已经安装了所需的软件包,您可以在应用程序中进行配置。打开文件并添加以下代码:
//src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { PrismaModule } from 'src/prisma/prisma.module';
export const jwtSecret = 'zjP9h6ZI5LoSKCRj';
@Module({
imports: [
PrismaModule,
PassportModule,
JwtModule.register({
secret: jwtSecret,
signOptions: { expiresIn: '5m' }, // e.g. 30s, 7d, 24h
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
@nestjs/passport
提供了 PassportModule
,您可以在官方文档中深入了解其相关信息。您已经配置了 PassportModule
以及一个用于生成和验证 JWT 的模块,这就是 JwtModule
,它依赖于 jsonwebtoken
库。JwtModule
提供了一个用于对 JWT 进行签名的密钥(jwtSecret
)以及定义了 JWT 过期时间的对象(expiresIn
),当前设置为 5 分钟。
注意:如果前一个令牌已过期,请记得生成新令牌。
您可以使用代码片段中提供的代码来生成 jwtSecret
,也可以使用 OpenSSL 生成自己的密钥。
注意:在实际应用程序中,切勿将密钥(
secret
)直接存储在代码库中。NestJS 提供了@nestjs/config
包,用于从环境变量中加载密钥。您可以查阅官方文档以获取更多信息。
实现终端节点
实现 POST /auth/login
终端节点。这个终端节点将用于用户身份验证。它将接收用户名和密码作为输入,如果凭证有效,则返回 JWT。首先,您需要创建一个类来定义 POST /login
请求正文的形状,这个类可以命名为 LoginDto
。
在src/auth/dto
目录下,创建一个名为login.dto.ts
的新文件:
mkdir src/auth/dto
touch src/auth/dto/login.dto.ts
现在,定义一个名为LoginDto
的类,该类包含email
和password
两个字段。
//src/auth/dto/login.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class LoginDto {
@IsEmail()
@IsNotEmpty()
@ApiProperty()
email: string;
@IsString()
@IsNotEmpty()
@MinLength(6)
@ApiProperty()
password: string;
}
您还需要定义一个类型来描述JWT有效负载的结构。请在src/auth/entity
目录下创建一个名为AuthEntity.ts
的新文件。
mkdir src/auth/entity
touch src/auth/entity/auth.entity.ts
现在在AuthEntity
文件中定义 :
//src/auth/entity/auth.entity.ts
import { ApiProperty } from '@nestjs/swagger';
export class AuthEntity {
@ApiProperty()
accessToken: string;
}
AuthEntity
应包含一个名为accessToken
的字符串字段,该字段用于存储JWT。
现在,在AuthService
中创建一个新的方法login
。
//src/auth/auth.service.ts
import {
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from './../prisma/prisma.service';
import { JwtService } from '@nestjs/jwt';
import { AuthEntity } from './entity/auth.entity';
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService, private jwtService: JwtService) {}
async login(email: string, password: string): Promise<AuthEntity> {
// Step 1: Fetch a user with the given email
const user = await this.prisma.user.findUnique({ where: { email: email } });
// If no user is found, throw an error
if (!user) {
throw new NotFoundException(`No user found for email: ${email}`);
}
// Step 2: Check if the password is correct
const isPasswordValid = user.password === password;
// If password does not match, throw an error
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid password');
}
// Step 3: Generate a JWT containing the user's ID and return it
return {
accessToken: this.jwtService.sign({ userId: user.id }),
};
}
}
该方法首先会尝试获取与给定电子邮件匹配的用户。如果用户不存在,则会抛出NotFoundException
,如果用户存在,接下来会验证提供的密码是否正确。如果密码验证失败,会抛出UnauthorizedException
。只有当密码验证成功时,该方法才会生成一个包含用户ID的JWT,并将其作为响应返回。
现在,在AuthController
中创建一个处理POST请求的方法,路径为/auth/login
。
//src/auth/auth.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AuthEntity } from './entity/auth.entity';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
@ApiTags('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@ApiOkResponse({ type: AuthEntity })
login(@Body() { email, password }: LoginDto) {
return this.authService.login(email, password);
}
}
现在,您的API中应该新增了一个终端节点,即POST /auth/login。
请访问http://localhost:3000/api
页面,并尝试访问这个新的终端节点。在发起POST /auth/login请求时,请使用您在种子脚本中创建的用户凭据进行身份验证。
您可以使用以下请求正文:
{
"email": "sabin@adams.com",
"password": "password-sabin"
}
执行请求后,您应该会在响应中获得 JWT。
在下一部分中,您将使用此令牌对用户进行身份验证。
实施 JWT 身份验证策略
在Passport中,策略负责验证请求的身份,通过实现特定的身份验证逻辑来完成这一任务。在本节中,我们将实现一个JWT身份验证策略,用于用户的身份验证。
我们不会直接使用Passport包,而是与@nestjs/passport
这个包装器包进行交互,它会在后台调用Passport。为了配置策略,我们需要创建一个扩展自PassportStrategy
的类。在这个过程中,主要需要完成两项工作:
- 将JWT策略特定的选项和配置传递给构造函数中的
super()
方法。 - 实现一个
validate()
回调方法,该方法会与数据库进行交互,根据JWT有效负载来获取用户。如果成功找到用户,该方法应返回一个user
对象。
首先,请在src/auth/strategy
目录下创建一个名为jwt.strategy.ts
的新文件。
touch src/auth/jwt.strategy.ts
现在实现类JwtStrategy
:
//src/auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { jwtSecret } from './auth.module';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecret,
});
}
async validate(payload: { userId: number }) {
const user = await this.usersService.findOne(payload.userId);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
您已经创建了一个继承自 PassportStrategy
的类,该类采用两个参数:策略实现和策略名称。您使用的是 passport-jwt
库中的预定义策略 JwtStrategy
。
在构造函数中,您向 super()
方法传递了一些选项。这些选项包括一个用于从请求中提取 JWT 的方法(在此情况下,您使用的是从 API 请求的 Authorization
标头中提供 JWT 的标准方法),以及一个用于验证 JWT 的密钥(secretOrKey
)。passport-jwt
存储库中还有更多可选的配置项。
对于 JwtStrategy
,Passport 首先会验证 JWT 的签名并解码其 JSON 有效负载。然后,Passport 会将解码后的 JSON 对象传递给 validate()
方法。由于 JWT 签名的工作机制,您可以确信接收到的令牌是在应用程序之前签名并颁发的有效牌。validate()
方法应该返回一个 user
对象。如果未找到对应的用户,该方法应该抛出一个错误。
注意:Passport 可能会让人感到有些困惑。将 Passport 本身视为一个迷你框架是有帮助的,它将身份验证过程抽象为几个步骤,这些步骤可以通过策略和配置选项进行自定义。我建议您阅读 NestJS 的 Passport 配方,以了解如何将 Passport 与 NestJS 结合使用的更多信息。
最后,在 AuthModule
中,您需要将新创建的 JwtStrategy
类作为提供程序(provider)添加进去:
//src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { PrismaModule } from 'src/prisma/prisma.module';
import { UsersModule } from 'src/users/users.module';
import { JwtStrategy } from './jwt.strategy';
export const jwtSecret = 'zjP9h6ZI5LoSKCRj';
@Module({
imports: [
PrismaModule,
PassportModule,
JwtModule.register({
secret: jwtSecret,
signOptions: { expiresIn: '5m' }, // e.g. 7d, 24h
}),
UsersModule,
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
现在,JwtStrategy
可以被其他模块使用了。您已经在UsersModule
中添加了JwtStrategy
,因为JwtStrategy
类正在使用UsersService
。
为了确保JwtStrategy
类在其他模块中的可访问性,您还需要在UsersModule
的exports
数组中添加JwtStrategy
。同时,由于UsersService
在JwtStrategy
类中被使用,确保UsersModule
已经正确地通过imports
引入了提供UsersService
的模块。
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [PrismaModule],
exports: [UsersService],
})
export class UsersModule {}
实施 JWT 身份验证保护
guard是 NestJS 中的一个结构,用于决定是否允许请求继续处理。在本节中,您将实现一个自定义guard,用于保护那些需要身份验证的路由。这个自定义guard将被命名为 JwtAuthGuard
。
请在 src/auth
目录下创建一个新文件,命名为 jwt-auth.guard.ts
。
touch src/auth/jwt-auth.guard.ts
现在实现类JwtAuthGuard
:
//src/auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
该类(指JwtStrategy
类)需要指定策略的名称。在本例中,您使用的是在上一节中实现的JWT策略,该策略的名称为jwt
。
现在,您可以将JwtAuthGuard
作为装饰器来使用,以保护您的API端点。请在UsersController
中,为您想要保护的路由添加JwtAuthGuard
。
// src/users/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
UseGuards,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
@Controller('users')
@ApiTags('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@ApiCreatedResponse({ type: UserEntity })
async create(@Body() createUserDto: CreateUserDto) {
return new UserEntity(await this.usersService.create(createUserDto));
}
@Get()
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: UserEntity, isArray: true })
async findAll() {
const users = await this.usersService.findAll();
return users.map((user) => new UserEntity(user));
}
@Get(':id')
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: UserEntity })
async findOne(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.findOne(id));
}
@Patch(':id')
@UseGuards(JwtAuthGuard)
@ApiCreatedResponse({ type: UserEntity })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return new UserEntity(await this.usersService.update(id, updateUserDto));
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: UserEntity })
async remove(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.remove(id));
}
}
如果您尝试在未经身份验证的情况下查询这些终端节点中的任何一个,它将不再有效。
在 Swagger 中集成身份验证
目前,Swagger 文档尚未标明哪些终端节点受到身份验证保护。您可以在控制器上添加一个装饰器: @ApiBearerAuth()
,以指明这些终端节点需要身份验证。
// src/users/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
UseGuards,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiBearerAuth, ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
@Controller('users')
@ApiTags('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@ApiCreatedResponse({ type: UserEntity })
async create(@Body() createUserDto: CreateUserDto) {
return new UserEntity(await this.usersService.create(createUserDto));
}
@Get()
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserEntity, isArray: true })
async findAll() {
const users = await this.usersService.findAll();
return users.map((user) => new UserEntity(user));
}
@Get(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserEntity })
async findOne(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.findOne(id));
}
@Patch(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiCreatedResponse({ type: UserEntity })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return new UserEntity(await this.usersService.update(id, updateUserDto));
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserEntity })
async remove(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.remove(id));
}
}
现在,受身份验证保护的终端节点在 Swagger 🔓 中应该有一个锁图标
由于目前无法在Swagger中直接进行身份验证,因此您可以通过测试来验证这些终端节点的功能。为此,您可以在main.ts
文件中,通过调用SwaggerModule
的.addBearerAuth()
方法,为Swagger添加Bearer令牌认证支持。
// src/main.ts
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
const config = new DocumentBuilder()
.setTitle('Median')
.setDescription('The Median API description')
.setVersion('0.1')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
现在,您可以通过点击 Swagger 界面中的“Authorize”按钮来添加 JWT 令牌。Swagger 会自动将这个令牌添加到您的请求头中,这样您就可以访问那些受保护的终端节点了。
注意:您可以通过向
/auth/login
终端节点发送包含有效电子邮件和密码的请求来生成 JWT 令牌。
自己动手试试吧。
哈希密码
目前,User.password
字段是以纯文本形式存储的,这存在安全风险,因为一旦数据库被泄露,所有密码也将暴露无遗。为了解决这个问题,我们可以在将密码存储到数据库之前,使用哈希处理来对密码进行加密。
您可以使用bcrypt
这个加密库来对密码进行哈希处理。要使用bcrypt
,请先通过npm进行安装:
npm install bcrypt
npm install --save-dev @types/bcrypt
首先,您将更新 UsersService
中的 create
和 update
方法,以确保在将密码存储到数据库之前对其进行哈希处理。
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import * as bcrypt from 'bcrypt';
export const roundsOfHashing = 10;
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async create(createUserDto: CreateUserDto) {
const hashedPassword = await bcrypt.hash(
createUserDto.password,
roundsOfHashing,
);
createUserDto.password = hashedPassword;
return this.prisma.user.create({
data: createUserDto,
});
}
findAll() {
return this.prisma.user.findMany();
}
findOne(id: number) {
return this.prisma.user.findUnique({ where: { id } });
}
async update(id: number, updateUserDto: UpdateUserDto) {
if (updateUserDto.password) {
updateUserDto.password = await bcrypt.hash(
updateUserDto.password,
roundsOfHashing,
);
}
return this.prisma.user.update({
where: { id },
data: updateUserDto,
});
}
remove(id: number) {
return this.prisma.user.delete({ where: { id } });
}
}
bcrypt.hash
函数接受两个参数:需要哈希的输入字符串(通常是密码),以及哈希的轮数(也被称为成本因子)。增加哈希的轮数会提升计算哈希值所需的时间,从而在安全性和性能之间取得平衡。哈希轮数越多,计算哈希的过程就越耗时,这有助于抵御暴力破解攻击。然而,更多的哈希轮数也意味着在用户登录时,系统需要花费更多时间来计算哈希值。关于这个话题,在Stack Overflow上进行了深入的讨论。
此外,bcrypt
还会自动应用一种称为加盐的技术,以增强密码哈希的安全性。加盐是指在哈希处理之前,向输入字符串中添加一个随机生成的字符串。这样做的好处是,即使两个用户使用了相同的密码,由于每个密码都附加了不同的盐值,它们最终生成的哈希值也会不同。这样,攻击者就无法利用预先计算好的哈希表来破解密码了。
您还需要更新数据库种子脚本,以便在将密码插入数据库之前对其进行哈希处理:
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
// initialize the Prisma Client
const prisma = new PrismaClient();
const roundsOfHashing = 10;
async function main() {
// create two dummy users
const passwordSabin = await bcrypt.hash('password-sabin', roundsOfHashing);
const passwordAlex = await bcrypt.hash('password-alex', roundsOfHashing);
const user1 = await prisma.user.upsert({
where: { email: 'sabin@adams.com' },
update: {
password: passwordSabin,
},
create: {
email: 'sabin@adams.com',
name: 'Sabin Adams',
password: passwordSabin,
},
});
const user2 = await prisma.user.upsert({
where: { email: 'alex@ruheni.com' },
update: {
password: passwordAlex,
},
create: {
email: 'alex@ruheni.com',
name: 'Alex Ruheni',
password: passwordAlex,
},
});
// create three dummy posts
// ...
}
// execute the main function
// ...
运行种子脚本后,您应该会发现存储在数据库中的密码已经经过了哈希处理。您可以使用以下命令来执行种子脚本:npx prisma db seed
。
...
Running seed command `ts-node prisma/seed.ts` ...
{
user1: {
id: 1,
name: 'Sabin Adams',
email: 'sabin@adams.com',
password: '$2b$10$XKQvtyb2Y.jciqhecnO4QONdVVcaghDgLosDPeI0e90POYSPd1Dlu',
createdAt: 2023-03-20T22:05:56.758Z,
updatedAt: 2023-04-02T22:58:05.792Z
},
user2: {
id: 2,
name: 'Alex Ruheni',
email: 'alex@ruheni.com',
password: '$2b$10$0tEfezrEd1a2g51lJBX6t.Tn.RLppKTv14mucUSCv40zs5qQyBaw6',
createdAt: 2023-03-20T22:05:56.772Z,
updatedAt: 2023-04-02T22:58:05.808Z
},
...
每次使用不同的盐值进行哈希处理时,所得到的哈希值(对于password
字段)都会有所不同。重要的是,现在该值是以哈希字符串的形式存储的。
然而,如果您现在尝试使用正确的密码进行登录,可能会遇到 HTTP 401 错误。这是因为当前的登录方法(login
)试图将用户请求中提供的纯文本密码直接与数据库中的哈希密码进行比较。
为了解决这个问题,您需要更新登录方法,以便在比较之前对提供的密码进行哈希处理。
//src/auth/auth.service.ts
import { AuthEntity } from './entity/auth.entity';
import { PrismaService } from './../prisma/prisma.service';
import {
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService, private jwtService: JwtService) {}
async login(email: string, password: string): Promise<AuthEntity> {
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) {
throw new NotFoundException(`No user found for email: ${email}`);
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid password');
}
return {
accessToken: this.jwtService.sign({ userId: user.id }),
};
}
}
您现在可以使用正确的密码登录,并在响应中获取 JWT。
总结和结束语
在本章中,您学习了如何在 NestJS REST API 中实现 JWT 身份验证。同时,您也掌握了如何对密码进行加盐处理,以及如何将身份验证功能与 Swagger 集成。
您可以在 end-authentication
分支中找到相关代码。如果您在操作过程中遇到问题,请随时在存储库中提出问题或提交 PR。此外,您也可以直接在 Twitter 上与我取得联系。
原文链接:https://www.prisma.io/blog/nestjs-prisma-authentication-7D056s1s0k3l