从 0 到 1 学习 NestJS (五) - 身份验证与授权 (Passport.js + JWT)

前言

在前面的文章中,我们学习了 NestJS 的核心概念、数据库集成以及如何使用 TypeORM。本篇将深入探讨如何在 NestJS 应用中实现身份验证和授权,我们将使用 Passport.js 作为身份验证中间件,并使用 JWT (JSON Web Tokens) 来管理用户会话。

1. 安装必要的依赖

首先,我们需要安装 Passport.js、JWT 和相关依赖:

1
2
3
npm install @nestjs/passport passport passport-jwt jsonwebtoken bcrypt @types/passport-jwt
# 或者
yarn add @nestjs/passport passport passport-jwt jsonwebtoken bcrypt @types/passport-jwt
@nestjs/passport: NestJS 官方提供的 Passport.js 集成模块。
passport: Passport.js 本身。
passport-jwt: Passport.js 的 JWT 策略。
jsonwebtoken: 用于生成和验证 JWT 的库。
bcrypt: 用于密码哈希。
@types/passport-jwt: Passport-JWT 的 TypeScript 类型定义。

创建身份验证模块

创建 auth 模块

使用 Nest CLI 创建一个新的模块:

1
nest g module auth

创建 auth.service.ts

在 src/auth 目录下创建一个 auth.service.ts 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}

async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOneByUsername(username);
if (user && await bcrypt.compare(pass, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}

async login(user: any) {
const payload = { username: user.username, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
}
validateUser(): 验证用户名和密码,成功返回用户信息。
login(): 生成 JWT。

创建 jwt.strategy.ts

在 src/auth 目录下创建一个 jwt.strategy.ts 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'your_secret_key', // 替换为你的密钥
});
}

async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
JwtStrategy: 定义 JWT 验证策略。
secretOrKey: 用于验证 JWT 的密钥,请替换为更安全的密钥。
validate(): 验证 JWT 并返回用户信息。

创建 auth.module.ts

在 src/auth 目录下创建一个 auth.module.ts 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';

@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: 'your_secret_key', // 替换为你的密钥
signOptions: { expiresIn: '60s' }, // 设置过期时间
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
JwtModule.register(): 配置 JWT 模块,包括密钥和过期时间。
providers: 注册 AuthService 和 JwtStrategy。
exports: 导出 AuthService,以便其他模块可以使用。

在 UsersModule 中添加用户注册和登录

修改 users.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

async create(user: Partial<User>): Promise<User> {
const hashedPassword = await bcrypt.hash(user.password, 10);
const newUser = this.usersRepository.create({...user, password: hashedPassword});
return await this.usersRepository.save(newUser);
}

async findOneByUsername(username: string): Promise<User | undefined> {
return await this.usersRepository.findOne({ where: { username } });
}
}
添加 findOneByUsername() 方法用于查找用户。
使用 bcrypt 对密码进行哈希。

修改 users.controller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/users/users.controller.ts
import { Controller, Post, Body, UseGuards, Get, Request } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { AuthService } from '../auth/auth.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';

@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService,
private authService: AuthService
) {}

@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
return await this.usersService.create(createUserDto);
}

@Post('login')
async login(@Body() user: any) {
return await this.authService.login(user);
}

@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
添加 register 路由用于用户注册。
添加 login 路由用于用户登录。
添加 profile 路由,使用 JwtAuthGuard 保护,需要 JWT 验证。

创建 jwt-auth.guard.ts

在 src/auth 目录下创建一个 jwt-auth.guard.ts 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/auth/jwt-auth.guard.ts
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}

handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}

4. 测试

现在,你可以运行 npm run start:devyarn start:dev,然后使用 Postman 或其他工具测试 API:

  1. POST /users/register: 注册一个新用户,请求体为:{"username": "testuser", "password": "testpassword", "email": "test@example.com"}。如果注册成功,你应该会收到一个包含新用户信息的响应。

  2. POST /users/login: 使用注册的用户名和密码登录,请求体为:{"username": "testuser", "password": "testpassword"}。如果登录成功,你应该会收到一个包含 access_token 的 JSON 响应。

  3. GET /users/profile: 使用上一步获取的 access_token 发起请求。在 Postman 中,将 Authorization 请求头设置为 Bearer <access_token>(将 <access_token> 替换为实际的 token)。如果验证成功,你应该会收到包含用户信息的响应。

总结

在本篇文章中,我们学习了如何在 NestJS 应用中实现身份验证和授权,使用 Passport.js 和 JWT。我们了解了如何创建 Passport 策略、生成和验证 JWT,以及如何使用 Guard 来保护路由。

通过本篇的学习,你已经掌握了在 NestJS 中实现基本的身份验证和授权的方法,可以为你的应用添加安全机制。