NestJS实战-创建AuthService

创建AuthService

新建 src/auth/auth.service.ts 文件,编写 AuthService 类来提供服务,主要实现:

  • validateUser:查询数据库验证用户的账号和密码是否正确,返回用户信息
  • createToken:生成 Token
  • login:拼接 Token 给到前端
typescript 复制代码
// src/auth/auth.service.ts
import { Injectable, Logger, Req, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/user/entities/user.entity';
import { removeUserData } from 'src/utils';
import { Repository } from 'typeorm';

@Injectable()
export class AuthService {
  private logger = new Logger('AuthService');

  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly jwtService: JwtService,
  ) {}

  async validateUser(account: string, password: string): Promise<User | null> {
    // 查询数据库来验证用户
    const user: User = await this.userRepository.findOne({
      where: { account, isDeleted: 0 },
    });
    // 如果有用户再验证密码
    if (
      user &&
      user.account === account &&
      (await user.validatePassword(password))
    ) {
      delete user.passwordHash;
      delete user.isDeleted;
      return user;
    }
    return null;
  }

  // 生成 Token
  async createToken(data) {
    return await this.jwtService.signAsync(data);
  }

  async login(user: any) {
    const payload = {
      account: user.account,
      userId: user.id,
      roleId: user.roleId,
      roleType: user.roleType,
      roleWeight: user.roleWeight,
    };
    const token = await this.createToken(payload);
    return {
      token,
    };
  }

  // 查询当前账户信息
  async queryCurrentUser(@Req() req) {
    try {
      if (req?.user?.userId) {
        const user = await this.userRepository.findOneBy({
          id: req.user.userId,
        });

        return removeUserData(user);
      }
    } catch (error) {
      this.logger.error('@@@@ 当前登录信息过期,请重新登录:', error);
      throw new UnauthorizedException('当前登录信息过期,请重新登录');
    }
  }
}

新建AuthController

新建 src/auth/auth.controller.ts 文件,编写 AuthController 类给前端提供接口,主要实现:

  • login:帐号登录,调用之前写的服务先进行用户账号密码验证,然后进行 token 获取,最后拼接信息给到前端
  • logout:帐号登出,也就是把 cookie 中的 token 设置过期就行
  • 查询当前账户信息:通过token查询当前登录账户信息
less 复制代码
import {
  Controller,
  Post,
  Body,
  BadRequestException,
  Req,
  Response,
} from '@nestjs/common';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';

@Controller('auth')
@ApiTags('身份验证')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  @ApiOperation({
    summary: '帐号登录',
    description: '根据帐号和密码登录',
  })
  @ApiBody({ type: LoginDto })
  @ApiResponse({ status: 200, description: '帐号登录成功' })
  async login(@Body() loginDto: LoginDto) {
    const user = await this.authService.validateUser(
      loginDto.account,
      loginDto.password,
    );

    if (!user) {
      throw new BadRequestException('帐号或密码不存在');
    }

    try {
      return await this.authService.login(user);
    } catch (err) {
      throw new BadRequestException('登录失败');
    }
  }

  @Post('logout')
  @ApiOperation({
    summary: '帐号登出',
    description: '帐号登出',
  })
  @ApiResponse({ status: 200, description: '帐号登出成功' })
  logout(@Req() req, @Response() res): void {
    // 清除cookie中的jwt
    res.cookie('jwt', '', { httpOnly: true, expires: new Date(0) });
    // 如果是localStorage存的token需要前端自己删除
    res.status(200).send({
      code: 200,
      msg: 'success',
      result: { message: '登出成功' },
    });
  }

  @Get('queryCurrentUser')
  @ApiOperation({
    summary: '查询当前账户信息',
    description: '通过token查询当前登录账户信息',
  })
  @ApiResponse({ status: 200, description: '账户信息查询成功' })
  queryCurrentUser(@Req() req) {
    return this.authService.queryCurrentUser(req);
  }
}

新建LoginDto

新建 src/auth/dto/login.dto.ts 文件:

less 复制代码
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsEmail, Matches } from 'class-validator';

export class LoginDto {
  @IsEmail({}, { message: '账号必须是邮箱格式' })
  @IsString({ message: '账号必须是字符串' })
  @IsNotEmpty({ message: '账号不能为空' })
  @ApiProperty({
    description: '账号(邮箱格式)',
    example: 'niunai@niunai.com',
  })
  account: string;

  @Matches(/^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/, {
    message: '请输入8-16位数字+字母的密码',
  })
  @IsString({ message: '密码必须是字符串' })
  @IsNotEmpty({ message: '密码不能为空' })
  @ApiProperty({ description: '密码', example: 'admin123' })
  password: string;
}

新建AuthModule

新建 src/auth/auth.module.ts 文件:

typescript 复制代码
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';
import { User } from 'src/user/entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtAuthGuard } from './jwt-auth.guard';

const jwtModule = JwtModule.register({
  global: true,
  secret: 'niunai', // 在生产环境中使用更安全的密钥管理方式
  signOptions: { expiresIn: '24h' }, // 设置 token 的过期时间
});

@Module({
  imports: [TypeOrmModule.forFeature([User]), PassportModule, jwtModule],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, JwtAuthGuard],
  exports: [AuthService, JwtAuthGuard], // 如果需要在其他模块中使用 AuthService
})
export class AuthModule {}

配置JWT策略

新建 src/auth/jwt.strategy.ts 文件,确保JWT策略使用 AuthService 来验证用户

typescript 复制代码
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: 'niunai', // 与 JwtModule.register 中的 secret 匹配
    });
  }

  async validate(payload: any) {
    // 这里返回的数据会被注入到 @Req.user 对象内
    return { 
      userId: payload.userId,
      account: payload.account,
      roleId: payload.roleId,
      roleType: payload.roleType,
      roleWeight: payload.roleWeight,
    };
  }
}

新建JwtAuthGuard路由守卫

新建 src/auth/jwt-auth.guard.ts 文件,可以用于全局、某个controller、某个接口进行jwt校验

typescript 复制代码
import {
  Injectable,
  ExecutionContext,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JwtService } from '@nestjs/jwt';
import { excludedRoutes } from './excluded.routes';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  private logger = new Logger('JwtAuthGuard');

  constructor(private readonly jwtService: JwtService) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<any> {
    const request = context.switchToHttp().getRequest();
    const { path, method } = request;

    // 检查当前请求是否在排除列表中
    const isExcluded = excludedRoutes.some(
      (route) => route.path === path && route.method === method,
    );

    if (isExcluded) {
      return true; // 如果请求被排除,则不进行 JWT 验证
    }

    const token = request.get('Authorization');

    this.logger.log('@@@@ token:', token);

    if (!token) {
      throw new UnauthorizedException('没有token,请先登录');
    }

    // Bearer token 格式
    const bearerToken = token.split(' ');

    if (bearerToken.length < 2 || bearerToken[0] !== 'Bearer') {
      throw new UnauthorizedException('token格式或类型不正确');
    }

    try {
      const decoded = await this.jwtService.verifyAsync(bearerToken[1]);
      // 令牌验证成功
      if (decoded) {
        request.user = decoded;
        return this.activate(context);
      }
    } catch (error) {
      throw new UnauthorizedException('没有权限,请登录');
    }
  }

  async activate(context: ExecutionContext): Promise<boolean> {
    return super.canActivate(context) as Promise<boolean>;
  }
}

其中 excludedRoutes 就是一个排除路由守卫的数组:

ini 复制代码
export const excludedRoutes = [{ path: '/auth/login', method: 'POST' }];

全局配置JWT路由守卫

我这边全局配置了 JWT 路由守卫,其实也可以某个 controller、某个接口进行 jwt 验证,但我的应用比较小,也不会有多个 jwt,就简单点使用了全局配置,至于排除就在路由里增加了一个 excludedRoutes 去排除就行了。

csharp 复制代码
// ...其他代码
import { JwtAuthGuard } from './auth/jwt-auth.guard';

async function bootstrap() {
  // ...其他代码
  app.useGlobalGuards(app.get(JwtAuthGuard));

  await app.listen(8004);
}
bootstrap();

权限管理功能接口完成

请求响应加密封装

后端 NestJS 使用 crypto 来实现数据加解密,使用 RSA 非对称加密算法和 AES 的对称加密算法进行混合加密,RSA 公钥对 AES 密钥进行加密,AES 对数据进行加密。

同时前端对接口的请求方法进行封装,提供 urlparamsconfig 参数给到调用方。

后端加密流程

  • 生成 RSA 密钥对:使用 OpenSSL 或其他工具生成 RSA 公钥和私钥。
  • 加密 AES 密钥:使用 RSA 公钥加密 AES 密钥。
  • 加密数据:使用 AES 对称加密算法加密实际的数据。
  • 发送加密信息:将加密后的 AES 密钥和加密后的数据发送给前端。

前端请求封装

  • 获取公钥:前端需要从后端获取 RSA 公钥。
  • 加密AES密钥:前端使用获取到的公钥加密 AES 密钥。
  • 加密数据:前端使用加密后的 AES 密钥加密请求数据。
  • 发送请求:前端将加密后的请求数据发送到后端。

目前这块我还没有做,后面有空补上

安全性

前后端都要做一些基础的安全性校验和拦截:

  • 数据验证 :对输入的数据进行必要的格式、类型和长度校验,避免 SQL 注入、XSS 等攻击。
  • 防范跨站脚本攻击 (XSS) :确保用户生成的内容在输出前进行转义处理,NestJS 内置的管道可以帮助自动转义输出。
  • 防止跨站请求伪造 (CSRF) :使用 CSRF 保护模块,为每个请求生成唯一的令牌,并将其与用户会话关联。

公共模块开发

新建 common 文件夹作为公共的模块

Excel文件导出

common 目录下新建 excel目录来存放控制器、服务和实例,用于给其他模块调用excel的导出能力

新建 excel/excel.service.ts

kotlin 复制代码
import * as XLSX from 'xlsx';
import { Injectable } from '@nestjs/common';

@Injectable()
export class ExcelService {
  /**
   * 将数据列表导出为 Excel 文件
   * @param data 数据列表
   * @param fileName 文件名
   * @returns 文件缓冲区
   */
  exportAsExcelFile(data: any[]): Buffer {
    const worksheet: XLSX.WorkSheet = XLSX.utils.json_to_sheet(data);
    const workbook: XLSX.WorkBook = {
      Sheets: { data: worksheet },
      SheetNames: ['data'],
    };
    const excelBuffer: any = XLSX.write(workbook, {
      bookType: 'xlsx',
      type: 'buffer',
    });
    return excelBuffer;
  }
}

新建 excel/excel.controller.ts

less 复制代码
import {
  Controller,
  Res,
  HttpStatus,
  Body,
  Post,
  BadRequestException,
} from '@nestjs/common';
import { ExcelService } from './excel.service';
import { ExcelDto } from './dto/excel.dto';
import { ApiBody, ApiTags } from '@nestjs/swagger';

@Controller('export')
@ApiTags('公共Excel导出')
export class ExcelController {
  constructor(private readonly excelService: ExcelService) {}

  @Post('/exportExcel')
  @ApiBody({ type: ExcelDto })
  exportExcel(@Body() body: ExcelDto, @Res() res) {
    try {
      // 导出为 Excel 文件
      const buffer = this.excelService.exportAsExcelFile(body.data);

      // 设置响应头
      res.setHeader(
        'Content-Type',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      );
      res.setHeader(
        'Content-Disposition',
        `attachment; filename=${encodeURIComponent(body.filename)}`,
      );

      // 发送文件
      res.status(HttpStatus.OK).send(buffer);
    } catch {
      throw new BadRequestException('Excel公共导出接口调用失败');
    }
  }
}

新建 excel/excel.module.ts

python 复制代码
import { Module } from '@nestjs/common';
import { ExcelController } from './excel.controller';
import { ExcelService } from './excel.service';

@Module({
  controllers: [ExcelController],
  providers: [ExcelService],
  exports: [ExcelService],
})
export class ExcelModule {}

新建实例 excel/dto/excel.dto.ts

less 复制代码
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class ExcelDto {
  @ApiProperty({
    description: 'excel名称',
    example: '用户表',
  })
  @IsNotEmpty({ message: 'filename必填' })
  filename: string;

  @ApiProperty({
    description: 'excel数据',
    example: [
      { id: 1, name: 'Alice', age: 25 },
      { id: 2, name: 'Bob', age: 30 },
      { id: 3, name: 'Charlie', age: 35 },
    ],
  })
  @IsNotEmpty({ message: 'data必填' })
  data: any[];
}

工具函数开发

新建 src/utils/index.ts 文件,用于在后续业务开发中对一些可复用的地方进行封装,目前主要是对一些表的用户信息设置进行封装:

ini 复制代码
import * as moment from 'moment';

// 设置创建用户信息
export const setCreatedUser = (req: any, table: any) => {
  const user = req?.user;
  table.createdBy = user?.userId;
  table.createdByAccount = user?.account;
  table.updatedBy = user?.userId;
  table.updatedByAccount = user?.account;

  return table;
};

// 设置更新用户信息
export const setUpdatedUser = (req: any, table: any) => {
  const user = req?.user;
  table.updatedBy = user?.userId;
  table.updatedByAccount = user?.account;

  return table;
};

// 设置删除用户信息
export const setDeletedUser = (req: any, table: any) => {
  const user = req?.user;
  table.updatedBy = user?.userId;
  table.updatedByAccount = user?.account;
  table.isDeleted = 1;

  return table;
};

// 移除一些非必要的数据
export const removeUnnecessaryData = (data: any) => {
  return data.map((item) => {
    const obj = {
      ...item,
      createdTime: item.createdTime
        ? moment(item.createdTime).format('YYYY-MM-DD HH:mm:ss')
        : '',
      updatedTime: item.updatedTime
        ? moment(item.updatedTime).format('YYYY-MM-DD HH:mm:ss')
        : '',
    };
    delete obj.passwordHash;
    delete obj.isDeleted;
    return obj;
  });
};

// 移除一些用户信息
export const removeUserData = (data: any) => {
  const filterData = data;
  delete filterData.createdBy;
  delete filterData.createdByAccount;
  delete filterData.createdTime;
  delete filterData.updatedBy;
  delete filterData.updatedByAccount;
  delete filterData.updatedTime;
  delete filterData.passwordHash;
  delete filterData.isDeleted;

  return filterData;
};
相关推荐
北冥有鱼1 小时前
mqtt 测试
前端·后端
代码丰1 小时前
使用 TtlExecutors 解决线程池中的 ThreadLocal 上下文丢失问题
后端
阿祖zu2 小时前
别再优化 RAG 了,适配 Agent 的 LLM Wiki 知识库理念
前端·后端·aigc
昵称为空C2 小时前
手撸一个动态 SQL 执行引擎:不重启服务,在线增删改查任意数据库
spring boot·后端
用户8356290780513 小时前
用 Python 自动化 PowerPoint 演讲者备注添加
后端·python
神奇小汤圆3 小时前
科研神器再升级!Claude Code 全套 Skills,16 大科研场景全覆盖!
后端
tyung3 小时前
Go 手写有界 SPSC 环形队列:无 CAS、无锁、Cache 友好的无锁模型
后端·go
咕白m6253 小时前
使用 C# 在 Excel 中应用多种字体样式
后端·c#
Java编程爱好者3 小时前
放弃 Spring AI?这 3 个开源框架,才是让 SpringBoot 玩转 AI Agent 的正解
后端