javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > NestJS创建AuthService

NestJS创建AuthService的实战示例

作者:赴星半途

本文主要介绍了NestJS创建AuthService的实战示例,包括创建estJSJS配置JWT策略、实现用户认证和权限管理、后端数据加密流程、前端接口请求封装、以及公共模块开发等如Excel导出功能,感兴趣的可以了解一下

创建AuthService

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

// 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 类给前端提供接口,主要实现:

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 文件:

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 文件:

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 来验证用户

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校验

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 就是一个排除路由守卫的数组:

export const excludedRoutes = [{ path: '/auth/login', method: 'POST' }];

全局配置JWT路由守卫

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

// ...其他代码
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 对数据进行加密。

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

后端加密流程

前端请求封装

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

安全性

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

公共模块开发

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

Excel文件导出

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

新建 excel/excel.service.ts

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

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

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

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 文件,用于在后续业务开发中对一些可复用的地方进行封装,目前主要是对一些表的用户信息设置进行封装:

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;
};

到此这篇关于NestJS创建AuthService的实战示例的文章就介绍到这了,更多相关NestJS创建AuthService内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文