├── 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)鍵文件和目錄概述如下:

請(qǐng)注意,如需了解這些組件的更多詳細(xì)信息,請(qǐng)參閱本教程系列的第一章。

在 REST API 中實(shí)施身份驗(yàn)證

在本節(jié)中,您將為 REST API 實(shí)現(xiàn)大部分身份驗(yàn)證邏輯。在本節(jié)結(jié)束時(shí),以下端點(diǎn)將受到身份驗(yàn)證保護(hù)??:

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)地回答問題:

  1. 請(qǐng)問您希望為這個(gè)資源使用什么名稱(請(qǐng)使用復(fù)數(shù)形式,例如“users”)?authentications
  2. 您使用的傳輸層是什么? REST API
  3. 您是否需要生成CRUD的入口點(diǎn)? 不

現(xiàn)在,您應(yīng)該在項(xiàng)目的目錄中找到一個(gè)名為auth的新模塊,它位于src/auth路徑下。

安裝和配置passport

passport是Node.js應(yīng)用程序中常用的一個(gè)身份驗(yàn)證庫。它配置靈活,并支持多種身份驗(yàn)證策略。passport旨在與構(gòu)建NestJS應(yīng)用的ExpressWeb框架協(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)終端節(jié)點(diǎ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的類,該類包含emailpassword兩個(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。

POST /auth/login 端點(diǎn)

在下一部分中,您將使用此令牌對(duì)用戶進(jìn)行身份驗(yàn)證。

實(shí)施 JWT 身份驗(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)工作:

  1. 將JWT策略特定的選項(xiàng)和配置傳遞給構(gòu)造函數(shù)中的super()方法。
  2. 實(shí)現(xiàn)一個(gè)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類在其他模塊中的可訪問性,您還需要在UsersModuleexports數(shù)組中添加JwtStrategy。同時(shí),由于UsersServiceJwtStrategy類中被使用,確保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 {}

實(shí)施 JWT 身份驗(yàn)證保護(hù)

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è),它將不再有效。

'GET /users 端點(diǎn)給出 401 響應(yīng)

在 Swagger 中集成身份驗(yàn)證

目前,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 中受身份驗(yàn)證保護(hù)的端點(diǎn)

由于目前無法在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)手試試吧。

Swagger 中的身份驗(yàn)證工作流

哈希密碼

目前,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。

總結(jié)和結(jié)束語

在本章中,您學(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

上一篇:

如何使用 Node.js 發(fā)送電子郵件:Email API或 Nodemailer (SMTP)(通過)

下一篇:

監(jiān)控使用 Apollo 和 Express 構(gòu)建的 GraphQL API
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)