第11章 nestjs服务端开发:登录鉴权

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

拦截器进阶(序列化):敏感数据如何处理?

https://www.mulingyuer.com/archives/979/

相关推荐
i建模2 小时前
如何在Arch Linux中重设忘记的root密码
linux·运维·服务器
chatexcel3 小时前
元空AI+Clawdbot:7×24 AI办公智能体新形态详解(长期上下文/自动化任务/工具粘合)
运维·人工智能·自动化
kida_yuan3 小时前
【Linux】运维实战笔记 — 我常用的方法与命令
linux·运维·笔记
何中应5 小时前
vmware的linux虚拟机如何设置以命令行方式启动
linux·运维·服务器
野犬寒鸦5 小时前
从零起步学习并发编程 || 第一章:初步认识进程与线程
java·服务器·后端·学习
江畔何人初6 小时前
kubernet与docker的关系
linux·运维·云原生
bubuly6 小时前
软件开发全流程注意事项:从需求到运维的全方位指南
大数据·运维·数据库
百炼成神 LV@菜哥6 小时前
Kylin Linux V10 aarch64 安装启动 TigerVNC-Server
linux·服务器·kylin
m0_737302586 小时前
百度智能云边缘云服务器,端云协同赋能全域智能场景
服务器