nestjs 用户认证:创建认证控制器
创建 auth module、service、controller 相关类
javascript
// 创建 auth module
nest g mo auth
// 创建 auth service
nest g s auth
//创建 auth controller
nest g co auth
src\auth\auth.module.ts
引入 userModule 模块,以便使用 userModule 的服务
javascript
@Module({
imports: [UserModule], // 引入user模块,
providers: [AuthService], // 注入服务
controllers: [AuthController], // 注入控制器
exports: [AuthService], // 导出服务
})
export class AuthModule {}
src\auth\auth.controller.ts
创建注册登录函数
javascript
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
// 登录
@Post('login')
sigin(@Body() dto: any) {
const { username, password } = dto;
return this.authService.signin(username, password);
}
// 注册
@Post('sigup')
sigup(@Body() dto: any) {
const { username, password } = dto;
return this.authService.sigup(username, password);
}
}
src\auth\auth.service.ts
借助 userService 实现登录注册功能
javascript
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { User } from '../user/user.entity';
@Injectable()
export class AuthService {
// 注入 userService
constructor(private readonly userService: UserService) {}
// 登录
async signin(username: string, password: string) {
const res = await this.userService.findAll({ username });
return res;
}
// 注册
sigup(username: any, password: any) {
return this.userService.create({
username,
password,
} as User);
}
}
src\user\user.module.ts
导出 userService 服务
javascript
@Module({
imports: [TypeOrmModule.forFeature([User, Logs])],
controllers: [UserController],
providers: [UserService],
exports: [UserService], // 导出服务,
})
export class UserModule {}
操作:创建和保存用户
src\auth\auth.controller.ts
注册用户参数校验
javascript
@Post('sigup')
sigup(@Body() dto: any) {
const { username, password } = dto;
// 用户名密码不能为空
if (!username || !password) {
return {
code: 400,
message: '用户名密码不能为空',
};
}
// 用户名密码类型不正确
if (typeof username !== 'string' || typeof password !== 'string') {
return {
code: 400,
message: '用户名密码类型不正确',
};
}
// 用户名密码长度不正确
if (username.length < 6 || password.length < 6) {
return {
code: 400,
message: '用户名密码长度必须超过6位',
};
}
return this.authService.sigup(username, password);
}
src\user\user.service.ts
实现用户注册逻辑
javascript
// 创建用户
// Partial<User> 表示 User 的所有属性都是可选的
async create(user: Partial<User>) {
// 如果 user.roles 不为空
if (!user.roles) {
const role = await this.roleRepository.findOne({
where: {
id: 2,
},
});
user.roles = [role];
}
if (user.roles instanceof Array && typeof user.roles[0] === 'string') {
// 查询所有用户角色
user.roles = await this.roleRepository.find({
where: {
id: In(user.roles),
},
});
}
const userTmp = await this.userRepository.create(user);
return this.userRepository.save(userTmp);
}
Nestjs管道概念:三大类型管道及校验类管道创建过程
管道概览
|----------|----------|-----------------------------|-----------|
| 管道类型 | 主要用途 | 内置示例 | 适用场景 |
| 验证管道 | 验证输入数据 | ValidationPipe | 表单验证、参数校验 |
| 转换管道 | 转换输入数据格式 | ParseIntPipe, ParseDatePipe | 类型转换、格式化 |
| 条件管道 | 条件性处理数据 | 自定义条件管道 | 路由守卫、权限验证 |
三大类型管道详解
验证管道(Validation Pipes)
主要用于验证请求数据的合法性。
TypeScript
// 1. 使用内置的 ValidationPipe
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局启用 ValidationPipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 自动去除没有装饰器的属性
forbidNonWhitelisted: true, // 如果有非白名单属性,抛出错误
transform: true, // 自动转换类型
disableErrorMessages: false, // 禁用错误消息
validationError: {
target: false, // 不在错误中返回目标对象
value: false, // 不在错误中返回原始值
},
}));
await app.listen(3000);
}
bootstrap();
转换管道(Transformation Pipes)
主要用于转换输入数据的格式或类型。
TypeScript
// 内置转换管道使用示例
import { Controller, Get, Param, Query, UsePipes, ParseIntPipe, ParseBoolPipe, ParseUUIDPipe, ParseArrayPipe } from '@nestjs/common';
import { ApiTags, ApiQuery } from '@nestjs/swagger';
@Controller('users')
@ApiTags('users')
export class UsersController {
// 1. ParseIntPipe - 转换为整数
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return { id, type: typeof id }; // id 自动转换为 number
}
// 2. ParseBoolPipe - 转换为布尔值
@Get('status/:isActive')
getByStatus(@Param('isActive', ParseBoolPipe) isActive: boolean) {
return { isActive, type: typeof isActive };
}
// 3. ParseUUIDPipe - 验证UUID格式
@Get('uuid/:uuid')
findByUuid(@Param('uuid', new ParseUUIDPipe({ version: '4' })) uuid: string) {
return { uuid };
}
// 4. ParseArrayPipe - 数组转换
@Get('ids')
findByIds(
@Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
ids: number[]
) {
return { ids };
}
// 5. ParseEnumPipe - 枚举验证
@Get('roles/:role')
findByRole(
@Param('role', new ParseEnumPipe(['admin', 'user', 'guest']))
role: string
) {
return { role };
}
}
条件管道(Conditional Pipes)
根据条件执行不同的验证逻辑
TypeScript
// 创建条件管道
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException, Optional } from '@nestjs/common';
@Injectable()
export class ConditionalValidationPipe implements PipeTransform {
constructor(
@Optional() private readonly condition: (value: any) => boolean,
@Optional() private readonly pipeIfTrue: PipeTransform,
@Optional() private readonly pipeIfFalse: PipeTransform
) {}
async transform(value: any, metadata: ArgumentMetadata) {
const shouldValidate = this.condition ? this.condition(value) : true;
if (shouldValidate && this.pipeIfTrue) {
return await this.pipeIfTrue.transform(value, metadata);
} else if (!shouldValidate && this.pipeIfFalse) {
return await this.pipeIfFalse.transform(value, metadata);
}
return value;
}
}
创建自定义管道
步骤1:创建基本管道结构
TypeScript
// src/pipes/custom.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class CustomValidationPipe implements PipeTransform {
// value: 输入的值
// metadata: 元数据,包含类型、数据类型、参数名等信息
transform(value: any, metadata: ArgumentMetadata) {
// 管道逻辑
return value;
}
}
步骤2:创建验证管道示例
示例1:Email 验证管道
TypeScript
// src/pipes/email-validation.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class EmailValidationPipe implements PipeTransform {
private readonly emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
transform(value: any) {
if (!this.isValidEmail(value)) {
throw new BadRequestException('Invalid email format');
}
return value.toLowerCase().trim();
}
private isValidEmail(email: string): boolean {
return this.emailRegex.test(email);
}
}
管道:nestjs 基于装饰器的验证使用类验证器
安装和配置
1. 安装依赖
javascript
# 核心依赖
npm install class-validator class-transformer
# 可选:类型定义
npm install -D @types/class-validator
# 可选:用于生成DTO类型
npm install @nestjs/mapped-types
# 可选:用于Swagger文档
npm install @nestjs/swagger
2. 全局配置 ValidationPipe
javascript
app.useGlobalPipes(new ValidationPipe());
基础验证装饰器
D:\Users\lhl\learn\nestjs\chapter11\nestjs-database-refactor\src\auth\dto\sigin-user.dto.ts
javascript
import { IsString, IsNotEmpty, Length } from 'class-validator';
export class SigninUserDto {
@IsString()
@IsNotEmpty()
@Length(6, 20,{
message: '用户名长度必须在6-20位之间'
})
username: string;
@IsString()
@IsNotEmpty()
@Length(6, 24,{
message: '密码长度必须在6-24位之间'
})
password: string;
}
回顾前置知识:完成JWT集成
安装依赖
"@nestjs/jwt": "^9.0.0",
"@nestjs/passport": "^9.0.3",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
src\enum\config.enum.ts
TypeScript
export enum ConfigEnum {
DB_TYPE = 'DB_TYPE',
DB_HOST = 'DB_HOST',
DB_PORT = 'DB_PORT',
DB_DATABASE = 'DB_DATABASE',
DB_USERNAME = 'DB_USERNAME',
DB_PASSWORD = 'DB_PASSWORD',
DB_SYNC = 'DB_SYNC',
SECRET = 'SECRET'
}
.env.production
TypeScript
# jwt secret
SECRET_KEY='long-random-secret-key'
src\auth\auth.module.ts
TypeScript
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from '../user/user.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigEnum } from 'src/enum/config.enum';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [UserModule,PassportModule,
// jwt模块
JwtModule.registerAsync({
// 异步加载jwt模块
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get(ConfigEnum.SECRET),
signOptions: {
expiresIn: '1d'
},
}
},
// 注入配置服务
inject: [ConfigService]
}),
], // 引入user模块,
providers: [AuthService], // 注入服务
controllers: [AuthController], // 注入控制器
exports: [AuthService], // 导出服务
})
export class AuthModule {}
src\auth\auth.strategy.ts
TypeScript
import { Injectable } from "@nestjs/common";
import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from "@nestjs/passport";
import { ConfigService } from "@nestjs/config";
import { ConfigEnum } from "src/enum/config.enum";
// jwt验证策略
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get(ConfigEnum.SECRET),
});
}
// 验证token
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
JWT实操:sign与解析jwt
src\auth\auth.service.ts
TypeScript
// 登录
async signin(username: string, password: string) {
const user = await this.userService.find(username);
if(user && user.password === password){
// 生成 token
return await this.jwt.signAsync({
sub: user.id,
username: user.username,
},{expiresIn: '1d'})
}
// 抛出异常
throw new UnauthorizedException()
}
src\auth\auth.controller.ts
TypeScript
// 登录
@Post('login')
sigin(@Body() dto: SigninUserDto) {
const { username, password } = dto;
const token = this.authService.signin(username, password);
return {
accessToken: token
}
}
src\user\user.controller.ts
TypeScript
@Get('/profile')
@UseGuards(AuthGuard('jwt'))
getUserProfile(@Query('id',ParseIntPipe) id: any): any {
return this.userService.findProfile(id);
}
鉴权守卫:设置鉴权用户可访问的控制器
src\guards\admin\admin.guard.ts
TypeScript
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { UserService } from '../../user/user.service';
import { User } from '../../user/user.entity';
@Injectable()
export class AdminGuard implements CanActivate {
constructor(private readonly userService: UserService) {}
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
// 获取请求对象
const request = context.switchToHttp().getRequest();
const user = (await this.userService.find(request.user.username)) as User;
// 普通用户
if(user.roles.filter(role => role.id === 2).length > 0) {
return true;
}
return false;
}
}
src\user\user.controller.ts
TypeScript
/**
* 1. 装饰器执行顺序: 方法的装饰器如果有多个,则从下往上执行
* 2. 如果使用 UseGuards 传递多个守卫,则从前往后执行,如果某个守卫返回 false,则后续守卫不会执行
* @returns
*/
@Get('/logs')
@UseGuards(AuthGuard('jwt'),AdminGuard)
getUserLogs(): any {
return this.userService.findUserLogs(2);
}
敏感信息操作:使用argon2库对密码进行加密
安装 argon2 库
TypeScript
"argon2": "0.30.2"
src\user\user.service.ts
密码加密
TypeScript
// 创建用户
// Partial<User> 表示 User 的所有属性都是可选的
async create(user: Partial<User>) {
// 如果 user.roles 不为空
if (!user.roles) {
const role = await this.roleRepository.findOne({
where: {
id: 2,
},
});
user.roles = [role];
}
if (user.roles instanceof Array && typeof user.roles[0] === 'string') {
// 查询所有用户角色
user.roles = await this.roleRepository.find({
where: {
id: In(user.roles),
},
});
}
const userTmp = await this.userRepository.create(user);
// 密码加密
userTmp.password = await argon2.hash(userTmp.password);
const res = await this.userRepository.save(userTmp);
return res
}
src\auth\auth.service.ts
校验密码
TypeScript
// 登录
async signin(username: string, password: string) {
const user = await this.userService.find(username);
if (!user) throw new ForbiddenException('用户不存在')
// 用户密码校验
const isMatch = await argon2.verify(user.password, password);
if (!isMatch) {
throw new ForbiddenException('用户名或密码错误')
}
// 生成 token
return await this.jwt.signAsync({
sub: user.id,
username: user.username,
}, { expiresIn: '1d' })
}
拦截器前导学习:删除敏感信息
https://juejin.cn/post/7431238822955319336