likes
comments
collection
share

NestJS博客实战12-给API做简单防护

作者站长头像
站长
· 阅读数 43

为什么使用JWT来保护接口?

在NestJS中使用JWT(JSON Web Tokens)来保护接口有几个原因:

  1. 身份验证和授权:JWT是一种用于身份验证和授权的开放标准。通过使用JWT,您可以在每个请求中传递令牌,并使用令牌验证用户身份。这使得在不使用传统会话(如使用Cookie或服务器端存储会话)的情况下进行身份验证成为可能。
  2. 无状态性:传统的基于会话的身份验证通常需要服务器在每个请求之间维护会话状态。而JWT是无状态的,所有用户相关的信息都被编码在令牌本身中。这意味着您的服务器不需要在每个请求之间存储任何会话数据,从而减轻了服务器的负担。
  3. 安全性:JWT可以使用加密算法进行签名,从而确保令牌的完整性和真实性。当服务器接收到JWT时,它可以验证签名以确保令牌未被篡改。这样可以防止攻击者通过篡改令牌来获取未经授权的访问权限。
  4. 可扩展性:JWT是一种标准化的令牌格式,广泛支持多种编程语言和框架。这使得JWT成为一种可扩展的身份验证解决方案,您可以在不同的系统之间共享令牌,并在各种前后端技术栈中使用。

在NestJS中,您可以使用官方提供的Passport模块结合JWT策略来实现JWT的身份验证和授权。它提供了一些方便的装饰器和中间件,可以帮助您轻松地集成JWT身份验证到您的NestJS应用程序中。

接下来我们就尝试着在项目导入这个功能。

改造user模块

之前我们写的user.service.ts用户创建接口,是直接把数据原本的内容放到数据库中的。这样用户的密码直接就暴露了,一般存储在数据库中的密码都应该要经过加密才行。

所以我改造了之前的创建用户模块,并将对密码进行加密。

  1. 首先安装加密的工具bcrypt(帮助您散列密码的库。)
pnpm i bcrypt
  1. 修改user.service.ts中的create创建用户接口,对密码进行加密。
import * as bcrypt from 'bcrypt';

@Injectable()
export class UserService {
。。。。
  /**
   * 创建用户
   * @param createUserDto
   * @returns
   */
  async create(createUserDto: CreateUserDto) {
    let res;

    const tempUser = await this.userRepository.create(createUserDto);
    // 给密码加密
    tempUser.password = this.encodePassword(tempUser.password);
    res = await this.userRepository.save(tempUser);

    if (res) {
      return 'success';
    } else {
      throw new BusinessException('用户创建失败');
    }
  }

  // 对用户密码进行编码
  public encodePassword(password: string): string {
    const salt = bcrypt.genSaltSync(10);
    return bcrypt.hashSync(password, salt);
  }
。。。
}
  1. 然后启动项目,并且请求创建接口http://localhost:8001/api/user/login,并传入下面的数据
{
    "username": "admintest",
    "password": "123456"
}
  1. 打开数据库确认结果,密码被加密了

NestJS博客实战12-给API做简单防护

创建Auth模块

安装jwt

pnpm install --save @nestjs/jwt

通过下面的命令创建Auth模块

nest g mo auth
nest g co auth --no-spec
nset g s auth --no-spec

安装依赖

pnpm i @nestjs/jwt

app.module.ts中注册JWT模块并且指定过期时间为1小时(过期时间可以根据自己的业务需求来定)

  imports: [
    JwtModule.register({
      signOptions: { expiresIn: '1h' },
    })
  ]

auth.controller.ts中添加一个login接口,代码如下

import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Public } from 'src/common';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

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

紧接着,写Service模块auth.service.ts

import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { ConfigEnum } from 'src/common/enum/config.enum';
import * as bcrypt from 'bcrypt';
import { BusinessException } from 'src/common/exceptions/business.exception';

@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwtService: JwtService,
    private configService: ConfigService,
  ) {}

  // 从用户数据库取得数据,并验证密码是否正确
  private async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.userService.findOne(username);
    if (user && this.isPasswordValid(pass, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  // 验证用户密码
  private isPasswordValid(password: string, userPassword: string): boolean {
    return bcrypt.compareSync(password, userPassword);
  }

  // 登陆接口
  async login(user: any) {
    const result: any = await this.validateUser(user.username, user.password);

    // 如果验证正确,则返回token,错误则直接抛出异常
    if (result) {
      const payload = { username: result.username, sub: result.userid };
      return {
        access_token: this.jwtService.sign(payload, {
          secret: this.configService.get(ConfigEnum.SECURITY_CONFIG).secret,
        }),
      };
    } else {
      BusinessException.throwForbidden();
    }
  }
}

请求接口确认

NestJS博客实战12-给API做简单防护

全局守卫

首先在common/guards文件夹下创建jwt.guard.ts

import {
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core';
import { ConfigEnum } from '../enum/config.enum';
import { ConfigService } from '@nestjs/config';
import { UserService } from 'src/modules/user/user.service';
import { BusinessException } from '../exceptions/business.exception';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,
    private reflector: Reflector,
    private configService: ConfigService,
    private userService: UserService,
  ) {}

  /**
   * 判断是否能通行
   * @param context 上下文
   * @returns 通行可否
   */
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 通过Request取得在Header中的token
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    // 如果不存在 token 则禁止放行
    if (!token) {
      BusinessException.throwForbidden();
    }
    try {
      // 验证token的内容
      const validate = await this.validate(token);
      if (validate) {
        // 解析token
        const payload = await this.jwtService.verifyAsync(token, {
          secret: this.configService.get(ConfigEnum.SECURITY_CONFIG).secret,
        });
        // 💡 我们在这里将有效载荷分配给请求对象
        // 以便我们可以在路由处理程序中访问它
        request['user'] = payload;
      }
    } catch {
      BusinessException.throwForbidden();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    // @ts-ignore
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }

  // 验证JWT令牌,如果JWT令牌无效则抛出禁止错误
  public async validate(token: string): Promise<boolean | never> {
    try {
      const decoded: any = this.jwtService.verify(token, {
        secret: this.configService.get(ConfigEnum.SECURITY_CONFIG).secret,
      });

      if (!decoded) {
        throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
      }
      const user = await this.validateUser(decoded);
      if (!user) {
        BusinessException.throwForbidden();
      }
      return true;
    } catch (err) {
      throw new HttpException(err.message.toUpperCase(), HttpStatus.FORBIDDEN);
    }
  }

  // 解码JWT令牌
  public async decode(token: string): Promise<any> {
    return this.jwtService.decode(token, null);
  }

  // 根据decode()中的userID验证用户
  public async validateUser(decoded: any) {
    return this.userService.findOneByCondition({
      where: { userid: decoded.userid },
    });
  }
}

并且,要让这个守卫全局生效,所以修改app.module.ts, 加入全局的守卫,这样写能够拥有NestJS依赖注入的能力。

@Module({
  providers: [
    ...
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AppModule {}

但是,并不是所有的接口一定要token验证,比如登陆接口,他本来就是用来取得token的,如果登陆接口也被守卫,那么我们将无法登陆,所有要有一种区分是否要验证接口的判断逻辑。

common文件夹下创建index.ts,在里面写一个简单的注解。

import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

然后修改 jwt.guard.ts 文件, 加入isPublic的判断

import { IS_PUBLIC_KEY } from '..';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  /**
   * 判断是否能通行
   * @param context 上下文
   * @returns 通行可否
   */
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // 如果是公共接口则放行
    if (isPublic) {
      // 💡 查看此条件
      return true;
    }

    // 通过Request取得在Header中的token
    const request = context.switchToHttp().getRequest();
    ....
}

auth.controller.ts中修改login接口,代码如下

import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Public } from 'src/common';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Public() // 不进行 jwt 验证
  @Post('login')
  async login(@Body() user) {
    return this.authService.login(user);
  }
}

测试接口

登陆取得token NestJS博客实战12-给API做简单防护

没有token访问接口 NestJS博客实战12-给API做简单防护

有token访问接口 NestJS博客实战12-给API做简单防护

其他

  1. Helmet

    Helmet可以通过适当设置HTTP标头来帮助保护您的应用程序免受一些众所周知的网络漏洞的攻击。一般来说,Helmet只是一个较小的中间件函数集合,用于设置与安全相关的HTTP头

安装依赖

pnpm i --save helmet

main.ts中加入helmet

import helmet from 'helmet';

app.use(helmet());
  1. 速率限制

    保护应用程序免受暴力攻击的一种常见技术是速率限制

安装依赖

pnpm i --save @nestjs/throttler

app.module.ts的修改,下面代码大致意思,60秒内最多只能接受10个请求,大家可以根据自己的服务器情况进行调节。

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,
      limit: 10,
    }),
  ],
})
export class AppModule {}

最后,谢谢大家支持,如果这篇文章对您有帮助,别忘了点赞/评论!🙏

本章代码

转载自:https://juejin.cn/post/7239363820875726908
评论
请登录