鍵.png)
使用這些基本 REST API 最佳實(shí)踐構(gòu)建出色的 API
├── 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
注意:此文件夾還附帶一個(gè)名為
test
的目錄,但本教程并不涵蓋測(cè)試內(nèi)容。然而,如果您對(duì)使用Prisma進(jìn)行應(yīng)用程序測(cè)試的最佳實(shí)踐感興趣,建議您查閱本教程系列的另一篇指南:使用Prisma進(jìn)行測(cè)試的終極指南。
此存儲(chǔ)庫中的關(guān)鍵文件和目錄概述如下:
src
目錄:包含應(yīng)用程序的源代碼。具體包括三個(gè)模塊:
app
模塊(位于src
根目錄下):作為應(yīng)用程序的入口點(diǎn),負(fù)責(zé)啟動(dòng)Web服務(wù)器。prisma
模塊:包含Prisma Client,這是您與數(shù)據(jù)庫交互的接口。articles
和users
模塊:分別定義了路由端點(diǎn)及其相關(guān)的業(yè)務(wù)邏輯。prisma
文件夾:
schema.prisma
文件:定義了數(shù)據(jù)庫的架構(gòu)。migrations
目錄:記錄了數(shù)據(jù)庫的遷移歷史。seed.ts
文件:包含一個(gè)腳本,用于使用虛擬數(shù)據(jù)為開發(fā)數(shù)據(jù)庫設(shè)定初始值。docker-compose.yml
文件:定義了PostgreSQL數(shù)據(jù)庫的Docker映像。.env
文件:包含了PostgreSQL數(shù)據(jù)庫的連接字符串。請(qǐng)注意,如需了解這些組件的更多詳細(xì)信息,請(qǐng)參閱本教程系列的第一章。
在本節(jié)中,您將為 REST API 實(shí)現(xiàn)大部分身份驗(yàn)證邏輯。在本節(jié)結(jié)束時(shí),以下端點(diǎn)將受到身份驗(yàn)證保護(hù)??:
GET /users
GET /users/:id
PATCH /users/:id
DELETE /users/:id
Web 上使用的身份驗(yàn)證主要有兩種類型:基于會(huì)話的身份驗(yàn)證和基于令牌的身份驗(yàn)證。在本教程中,您將使用 JSON Web 令牌 (JWT) 實(shí)現(xiàn)基于令牌的身份驗(yàn)證。
首先,在您的應(yīng)用程序中創(chuàng)建一個(gè)新模塊。運(yùn)行以下命令以生成新模塊。
npx nest generate resource
您將收到一些 CLI 提示,請(qǐng)相應(yīng)地回答問題:
現(xiàn)在,您應(yīng)該在項(xiàng)目的目錄中找到一個(gè)名為auth
的新模塊,它位于src/auth
路徑下。
passport
是Node.js應(yīng)用程序中常用的一個(gè)身份驗(yàn)證庫。它配置靈活,并支持多種身份驗(yàn)證策略。passport
旨在與構(gòu)建NestJS應(yīng)用的Express
Web框架協(xié)同工作。NestJS提供了與@nestjs/passport
的第一方集成,這使得在NestJS應(yīng)用程序中使用passport
變得十分簡(jiǎn)便。
首先安裝以下軟件包:
npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
現(xiàn)在您已經(jīng)安裝了所需的軟件包,您可以在應(yīng)用程序中進(jìn)行配置。打開文件并添加以下代碼:
//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
,您可以在官方文檔中深入了解其相關(guān)信息。您已經(jīng)配置了 PassportModule
以及一個(gè)用于生成和驗(yàn)證 JWT 的模塊,這就是 JwtModule
,它依賴于 jsonwebtoken
庫。JwtModule
提供了一個(gè)用于對(duì) JWT 進(jìn)行簽名的密鑰(jwtSecret
)以及定義了 JWT 過期時(shí)間的對(duì)象(expiresIn
),當(dāng)前設(shè)置為 5 分鐘。
注意:如果前一個(gè)令牌已過期,請(qǐng)記得生成新令牌。
您可以使用代碼片段中提供的代碼來生成?jwtSecret
,也可以使用 OpenSSL 生成自己的密鑰。
注意:在實(shí)際應(yīng)用程序中,切勿將密鑰(
secret
)直接存儲(chǔ)在代碼庫中。NestJS 提供了@nestjs/config
包,用于從環(huán)境變量中加載密鑰。您可以查閱官方文檔以獲取更多信息。
實(shí)現(xiàn)?POST /auth/login
?終端節(jié)點(diǎn)。這個(gè)終端節(jié)點(diǎn)將用于用戶身份驗(yàn)證。它將接收用戶名和密碼作為輸入,如果憑證有效,則返回 JWT。首先,您需要?jiǎng)?chuàng)建一個(gè)類來定義?POST /login
?請(qǐng)求正文的形狀,這個(gè)類可以命名為?LoginDto
。
在src/auth/dto
目錄下,創(chuàng)建一個(gè)名為login.dto.ts
的新文件:
mkdir src/auth/dto
touch src/auth/dto/login.dto.ts
現(xiàn)在,定義一個(gè)名為LoginDto
的類,該類包含email
和password
兩個(gè)字段。
//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;
}
您還需要定義一個(gè)類型來描述JWT有效負(fù)載的結(jié)構(gòu)。請(qǐng)?jiān)?code>src/auth/entity目錄下創(chuàng)建一個(gè)名為AuthEntity.ts
的新文件。
mkdir src/auth/entity
touch src/auth/entity/auth.entity.ts
現(xiàn)在在AuthEntity
文件中定義 :
//src/auth/entity/auth.entity.ts
import { ApiProperty } from '@nestjs/swagger';
export class AuthEntity {
@ApiProperty()
accessToken: string;
}
AuthEntity
應(yīng)包含一個(gè)名為accessToken
的字符串字段,該字段用于存儲(chǔ)JWT。
現(xiàn)在,在AuthService
中創(chuàng)建一個(gè)新的方法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 }),
};
}
}
該方法首先會(huì)嘗試獲取與給定電子郵件匹配的用戶。如果用戶不存在,則會(huì)拋出NotFoundException
,如果用戶存在,接下來會(huì)驗(yàn)證提供的密碼是否正確。如果密碼驗(yàn)證失敗,會(huì)拋出UnauthorizedException
。只有當(dāng)密碼驗(yàn)證成功時(shí),該方法才會(huì)生成一個(gè)包含用戶ID的JWT,并將其作為響應(yīng)返回。
現(xiàn)在,在AuthController
中創(chuàng)建一個(gè)處理POST請(qǐng)求的方法,路徑為/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);
}
}
現(xiàn)在,您的API中應(yīng)該新增了一個(gè)終端節(jié)點(diǎn),即POST /auth/login。
請(qǐng)?jiān)L問http://localhost:3000/api
頁面,并嘗試訪問這個(gè)新的終端節(jié)點(diǎn)。在發(fā)起POST /auth/login請(qǐng)求時(shí),請(qǐng)使用您在種子腳本中創(chuàng)建的用戶憑據(jù)進(jìn)行身份驗(yàn)證。
您可以使用以下請(qǐng)求正文:
{
"email": "sabin@adams.com",
"password": "password-sabin"
}
執(zhí)行請(qǐng)求后,您應(yīng)該會(huì)在響應(yīng)中獲得 JWT。
在下一部分中,您將使用此令牌對(duì)用戶進(jìn)行身份驗(yàn)證。
在Passport中,策略負(fù)責(zé)驗(yàn)證請(qǐng)求的身份,通過實(shí)現(xiàn)特定的身份驗(yàn)證邏輯來完成這一任務(wù)。在本節(jié)中,我們將實(shí)現(xiàn)一個(gè)JWT身份驗(yàn)證策略,用于用戶的身份驗(yàn)證。
我們不會(huì)直接使用Passport包,而是與@nestjs/passport
這個(gè)包裝器包進(jìn)行交互,它會(huì)在后臺(tái)調(diào)用Passport。為了配置策略,我們需要?jiǎng)?chuàng)建一個(gè)擴(kuò)展自PassportStrategy
的類。在這個(gè)過程中,主要需要完成兩項(xiàng)工作:
super()
方法。validate()
回調(diào)方法,該方法會(huì)與數(shù)據(jù)庫進(jìn)行交互,根據(jù)JWT有效負(fù)載來獲取用戶。如果成功找到用戶,該方法應(yīng)返回一個(gè)user
對(duì)象。首先,請(qǐng)?jiān)?code>src/auth/strategy目錄下創(chuàng)建一個(gè)名為jwt.strategy.ts
的新文件。
touch src/auth/jwt.strategy.ts
現(xiàn)在實(shí)現(xiàn)類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;
}
}
您已經(jīng)創(chuàng)建了一個(gè)繼承自 PassportStrategy
的類,該類采用兩個(gè)參數(shù):策略實(shí)現(xiàn)和策略名稱。您使用的是 passport-jwt
庫中的預(yù)定義策略 JwtStrategy
。
在構(gòu)造函數(shù)中,您向 super()
方法傳遞了一些選項(xiàng)。這些選項(xiàng)包括一個(gè)用于從請(qǐng)求中提取 JWT 的方法(在此情況下,您使用的是從 API 請(qǐng)求的 Authorization
標(biāo)頭中提供 JWT 的標(biāo)準(zhǔn)方法),以及一個(gè)用于驗(yàn)證 JWT 的密鑰(secretOrKey
)。passport-jwt
存儲(chǔ)庫中還有更多可選的配置項(xiàng)。
對(duì)于?JwtStrategy
,Passport 首先會(huì)驗(yàn)證 JWT 的簽名并解碼其 JSON 有效負(fù)載。然后,Passport 會(huì)將解碼后的?JSON 對(duì)象傳遞給?validate()
?方法。由于 JWT 簽名的工作機(jī)制,您可以確信接收到的令牌是在應(yīng)用程序之前簽名并頒發(fā)的有效牌。validate()
?方法應(yīng)該返回一個(gè)?user
?對(duì)象。如果未找到對(duì)應(yīng)的用戶,該方法應(yīng)該拋出一個(gè)錯(cuò)誤。
注意:Passport 可能會(huì)讓人感到有些困惑。將 Passport 本身視為一個(gè)迷你框架是有幫助的,它將身份驗(yàn)證過程抽象為幾個(gè)步驟,這些步驟可以通過策略和配置選項(xiàng)進(jìn)行自定義。我建議您閱讀 NestJS 的 Passport 配方,以了解如何將 Passport 與 NestJS 結(jié)合使用的更多信息。
最后,在 AuthModule
中,您需要將新創(chuàng)建的 JwtStrategy
類作為提供程序(provider)添加進(jìn)去:
//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 {}
現(xiàn)在,JwtStrategy
可以被其他模塊使用了。您已經(jīng)在UsersModule
中添加了JwtStrategy
,因?yàn)?code>JwtStrategy類正在使用UsersService
。
為了確保JwtStrategy
類在其他模塊中的可訪問性,您還需要在UsersModule
的exports
數(shù)組中添加JwtStrategy
。同時(shí),由于UsersService
在JwtStrategy
類中被使用,確保UsersModule
已經(jīng)正確地通過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 {}
guard是 NestJS 中的一個(gè)結(jié)構(gòu),用于決定是否允許請(qǐng)求繼續(xù)處理。在本節(jié)中,您將實(shí)現(xiàn)一個(gè)自定義guard,用于保護(hù)那些需要身份驗(yàn)證的路由。這個(gè)自定義guard將被命名為?JwtAuthGuard
。
請(qǐng)?jiān)?nbsp;src/auth
目錄下創(chuàng)建一個(gè)新文件,命名為 jwt-auth.guard.ts
。
touch src/auth/jwt-auth.guard.ts
現(xiàn)在實(shí)現(xiàn)類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
類)需要指定策略的名稱。在本例中,您使用的是在上一節(jié)中實(shí)現(xiàn)的JWT策略,該策略的名稱為jwt
。
現(xiàn)在,您可以將JwtAuthGuard
作為裝飾器來使用,以保護(hù)您的API端點(diǎn)。請(qǐng)?jiān)?code>UsersController中,為您想要保護(hù)的路由添加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));
}
}
如果您嘗試在未經(jīng)身份驗(yàn)證的情況下查詢這些終端節(jié)點(diǎn)中的任何一個(gè),它將不再有效。
目前,Swagger 文檔尚未標(biāo)明哪些終端節(jié)點(diǎn)受到身份驗(yàn)證保護(hù)。您可以在控制器上添加一個(gè)裝飾器:?@ApiBearerAuth()
,以指明這些終端節(jié)點(diǎn)需要身份驗(yàn)證。
// 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));
}
}
現(xiàn)在,受身份驗(yàn)證保護(hù)的終端節(jié)點(diǎn)在 Swagger ?? 中應(yīng)該有一個(gè)鎖圖標(biāo)
由于目前無法在Swagger中直接進(jìn)行身份驗(yàn)證,因此您可以通過測(cè)試來驗(yàn)證這些終端節(jié)點(diǎn)的功能。為此,您可以在main.ts
文件中,通過調(diào)用SwaggerModule
的.addBearerAuth()
方法,為Swagger添加Bearer令牌認(rèn)證支持。
// 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();
現(xiàn)在,您可以通過點(diǎn)擊 Swagger 界面中的“Authorize”按鈕來添加 JWT 令牌。Swagger 會(huì)自動(dòng)將這個(gè)令牌添加到您的請(qǐng)求頭中,這樣您就可以訪問那些受保護(hù)的終端節(jié)點(diǎn)了。
注意:您可以通過向
/auth/login
終端節(jié)點(diǎn)發(fā)送包含有效電子郵件和密碼的請(qǐng)求來生成 JWT 令牌。
自己動(dòng)手試試吧。
目前,User.password
字段是以純文本形式存儲(chǔ)的,這存在安全風(fēng)險(xiǎn),因?yàn)橐坏?shù)據(jù)庫被泄露,所有密碼也將暴露無遺。為了解決這個(gè)問題,我們可以在將密碼存儲(chǔ)到數(shù)據(jù)庫之前,使用哈希處理來對(duì)密碼進(jìn)行加密。
您可以使用bcrypt
這個(gè)加密庫來對(duì)密碼進(jìn)行哈希處理。要使用bcrypt
,請(qǐng)先通過npm進(jìn)行安裝:
npm install bcrypt
npm install --save-dev @types/bcrypt
首先,您將更新 UsersService
中的 create
和 update
方法,以確保在將密碼存儲(chǔ)到數(shù)據(jù)庫之前對(duì)其進(jìn)行哈希處理。
// 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
函數(shù)接受兩個(gè)參數(shù):需要哈希的輸入字符串(通常是密碼),以及哈希的輪數(shù)(也被稱為成本因子)。增加哈希的輪數(shù)會(huì)提升計(jì)算哈希值所需的時(shí)間,從而在安全性和性能之間取得平衡。哈希輪數(shù)越多,計(jì)算哈希的過程就越耗時(shí),這有助于抵御暴力破解攻擊。然而,更多的哈希輪數(shù)也意味著在用戶登錄時(shí),系統(tǒng)需要花費(fèi)更多時(shí)間來計(jì)算哈希值。關(guān)于這個(gè)話題,在Stack Overflow上進(jìn)行了深入的討論。
此外,bcrypt
還會(huì)自動(dòng)應(yīng)用一種稱為加鹽的技術(shù),以增強(qiáng)密碼哈希的安全性。加鹽是指在哈希處理之前,向輸入字符串中添加一個(gè)隨機(jī)生成的字符串。這樣做的好處是,即使兩個(gè)用戶使用了相同的密碼,由于每個(gè)密碼都附加了不同的鹽值,它們最終生成的哈希值也會(huì)不同。這樣,攻擊者就無法利用預(yù)先計(jì)算好的哈希表來破解密碼了。
您還需要更新數(shù)據(jù)庫種子腳本,以便在將密碼插入數(shù)據(jù)庫之前對(duì)其進(jìn)行哈希處理:
// 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
// ...
運(yùn)行種子腳本后,您應(yīng)該會(huì)發(fā)現(xiàn)存儲(chǔ)在數(shù)據(jù)庫中的密碼已經(jīng)經(jīng)過了哈希處理。您可以使用以下命令來執(zhí)行種子腳本: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
},
...
每次使用不同的鹽值進(jìn)行哈希處理時(shí),所得到的哈希值(對(duì)于password
字段)都會(huì)有所不同。重要的是,現(xiàn)在該值是以哈希字符串的形式存儲(chǔ)的。
然而,如果您現(xiàn)在嘗試使用正確的密碼進(jìn)行登錄,可能會(huì)遇到 HTTP 401 錯(cuò)誤。這是因?yàn)楫?dāng)前的登錄方法(login
)試圖將用戶請(qǐng)求中提供的純文本密碼直接與數(shù)據(jù)庫中的哈希密碼進(jìn)行比較。
為了解決這個(gè)問題,您需要更新登錄方法,以便在比較之前對(duì)提供的密碼進(jìn)行哈希處理。
//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 }),
};
}
}
您現(xiàn)在可以使用正確的密碼登錄,并在響應(yīng)中獲取 JWT。
在本章中,您學(xué)習(xí)了如何在 NestJS REST API 中實(shí)現(xiàn) JWT 身份驗(yàn)證。同時(shí),您也掌握了如何對(duì)密碼進(jìn)行加鹽處理,以及如何將身份驗(yàn)證功能與 Swagger 集成。
您可以在 end-authentication
分支中找到相關(guān)代碼。如果您在操作過程中遇到問題,請(qǐng)隨時(shí)在存儲(chǔ)庫中提出問題或提交 PR。此外,您也可以直接在 Twitter 上與我取得聯(lián)系。
原文鏈接:https://www.prisma.io/blog/nestjs-prisma-authentication-7D056s1s0k3l
對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力
一鍵對(duì)比試用API 限時(shí)免費(fèi)