美文网首页
跟随官网学nestjs之守卫

跟随官网学nestjs之守卫

作者: cc_licc | 来源:发表于2021-03-15 17:10 被阅读0次

    上篇讲到 Nest 的入门,这篇讲下在 Nest 中守卫,也叫授权。
    我们现在就以常见的身份认证作为示例,通过登录获取token,在特定的请求头需要带上token,并校验token是否过期

    身份认证

    Passport 是最流行的 node.js 身份验证库。将这个库与使用 @nestjs/passport 模块的 Nest 应用程序集成起来非常简单。这是官方推荐的一个库,接下来我们就使用该库来实现认证

    安装

    $ npm install --save @nestjs/passport passport passport-local
    $ npm install --save-dev @types/passport-local
    

    Passport 提供了一种名为 Passport-local 的策略,它实现了一种用户名/密码身份验证机制。关于其策略,官网都有详细的说明,我们接下来就看具体实现。

    实现

    我们新增一个 auth 模块,用于权限验证;在实现这块功能时,你就会发现我们之前写的 user 模块是很有用的。

    nest g controller auth
    nest g service auth
    nest g module auth
    

    生成 auth.controller.tsauth.service.tsauth.module.ts 文件,现在我们来完善它们

    • 新增登录接口

    auth.controller.ts

    import { Controller, Post, Body } from '@nestjs/common';
    import { ApiTags, ApiOperation, ApiForbiddenResponse, ApiNotFoundResponse } from '@nestjs/swagger';
    import { AuthService } from './auth.service';
    import { LoginDto } from './dto';
    
    @ApiTags('auth')
    @Controller('auth')
    export class AuthController {
      constructor(private readonly authService: AuthService) {}
    
      @ApiOperation({ summary: 'user login' })
      @ApiForbiddenResponse({ description: 'Forbidden' })
      @ApiNotFoundResponse({ description: 'Not Found' })
      @Post('/login')
      async login(@Body() _: LoginDto) {
        return this.authService.login(_);
      }
    }
    

    auth.service.ts 提供登录服务,并返回 token

    import { Injectable } from '@nestjs/common';
    
    @Injectable()
    export class AuthService {
      async login(user) {
        return {
          token: `Bearer token`,
        };
      }
    }
    

    auth.module.ts 提供 auth.service

    import { Module } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { AuthController } from './auth.controller';
    
    @Module({
      imports: [],
      providers: [AuthService],
      controllers: [AuthController],
    })
    export class AuthModule {}
    

    更新 app.module.ts

    import { Module } from '@nestjs/common';
    import { SequelizeModule } from '@nestjs/sequelize';
    import { UserModule } from './user/user.module';
    import { User } from './user/user.model';
    import { AuthModule } from './auth/auth.module';
    
    @Module({
      imports: [
        UserModule,
        AuthModule,
        SequelizeModule.forRoot({
          dialect: 'mysql',
          host: 'localhost',
          port: 3306,
          username: 'root',
          password: 'root',
          database: 'mysql_demo',
          models: [User],
        }),
      ],
      controllers: [],
      providers: [],
    })
    export class AppModule {}
    

    这时刷新浏览器,就能看见新增了一个接口,我们执行一下,就能看见接口返回的模拟 token

    img

    那么接下来我们就是需要完善登录这个服务了,我们的需求是用户输入账号密码,服务端去数据库查找,验证是否能够匹配的上,所以 service 的任务是检索用户并验证密码。我们提供一个 validateUser 方法,调用 user 模块的服务查找该用户的账号,并对比输入的密码与返回的密码是否一致

    auth.service.ts

    import { Injectable } from '@nestjs/common';
    import { UserService } from '../user/user.service';
    
    @Injectable()
    export class AuthService {
      constructor(
        private readonly userService: UserService,
      ) {}
    
      async validateUser(username: string, pass: string): Promise<any> {
        const user = await this.userService.findOneByName(username);
        if (user && user.password === pass) {
          const { password, ...result } = user;
          return result;
        }
        return null;
      }
    
      async login(user) {
        return {
          token: `Bearer token`,
        };
      }
    }
    

    user.service.ts 需要新增一个通过用户名查询用户的服务,并返回用户的密码

      async findOneByName(username: string): Promise<User> {
        return await this.userModel.findOne<User>({
          where: { username },
          attributes: ['id', 'username', 'password'],
        });
      }
    

    好了,现在查询用户的服务有了,校验用户身份的方法也有了,那么我们就可以实现 Passport 本地身份验证策略了。

    • Passport 本地策略

    auth 文件下新建 local.strategy.ts

    import { Strategy } from 'passport-local';
    import { PassportStrategy } from '@nestjs/passport';
    import { Injectable, UnauthorizedException } from '@nestjs/common';
    import { AuthService } from './auth.service';
    
    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy) {
      constructor(private readonly authService: AuthService) {
        super();
      }
    
      async validate(username: string, password: string): Promise<any> {
        const user = await this.authService.validateUser(username, password);
        if (!user) {
          throw new UnauthorizedException();
        }
        return user;
      }
    }
    

    passport-local 用例中,没有配置选项,因此我们的构造函数只是调用 super() ,没有 options 对象。

    对于每个策略,Passport 将使用适当的特定于策略的一组参数调用 verify 函数(使用 @nestjs/Passport 中的 validate() 方法实现)。对于本地策略,Passport 需要一个具有以下签名的 validate() 方法: validate(username: string, password: string): any。任何 Passport 策略的 validate() 方法都将遵循类似的模式,只是表示凭证的细节方面有所不同。如果找到了用户并且凭据有效,则返回该用户,以便 Passport 能够完成其任务,并且请求处理管道可以继续。如果没有找到,抛出一个异常,让异常层处理它。

    更新一下 auth.module

    import { Module } from '@nestjs/common';
    import { UserModule } from '../user/user.module';
    import { AuthService } from './auth.service';
    import { AuthController } from './auth.controller';
    import { PassportModule } from '@nestjs/passport';
    import { LocalStrategy } from './local.strategy';
    
    @Module({
      imports: [
        UserModule,
        PassportModule,
      ],
      providers: [AuthService, LocalStrategy],
      controllers: [AuthController],
    })
    export class AuthModule {}
    

    内置守卫

    守卫的主要功能:确定请求是否由路由处理程序。简单来说,我们针对不同的路由可能会存在不同的权限管控,如:auth/login 我们就需要用户名/密码凭证来启动身份验证;而其他接口(获取用户信息)我们就需要启用令牌 token(也就是 JWT 机制)来检验。

    当然 @nestjs/passport 模块为我们提供了一个内置的守卫,我们可以应用内置的守卫来启动护照本地流。

    auth.controller.ts 为其添加本地守卫

    import { Controller, Post, Body, UseGuards } from '@nestjs/common';
    import { ApiTags, ApiOperation, ApiForbiddenResponse, ApiNotFoundResponse } from '@nestjs/swagger';
    import { AuthService } from './auth.service';
    import { LoginDto } from './dto';
    import { LocalAuthGuard } from './local-auth.guard';
    
    @ApiTags('auth')
    @Controller('auth')
    export class AuthController {
      constructor(private readonly authService: AuthService) {}
    
      @ApiOperation({ summary: 'user login' })
      @ApiForbiddenResponse({ description: 'Forbidden' })
      @ApiNotFoundResponse({ description: 'Not Found' })
      @UseGuards(LocalAuthGuard)
      @Post('/login')
      async login(@Body() _: LoginDto) {
        return this.authService.login(_);
      }
    }
    

    这里 @UseGuards(LocalAuthGuard) 就是一个守卫,也有这样写的 @UseGuards(AuthGuard('local') ;对于前者,我们需新建一个 guard 文件来负责,其实现很简单

    local-auth.guard.ts

    import { Injectable } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    
    @Injectable()
    export class LocalAuthGuard extends AuthGuard('local') {}
    

    这样的好处是,我们能清楚的知道每个守卫自己的职责,随着业务的扩展,守卫也会随之增加,也方便对其守卫进行一些扩展。

    Passport 会根据从 validate 方法返回的值自动创建一个 user 对象,并将其作为 req.user 分配给请求对象。因此我们需要修改 auth.controller.ts 来接受这个返回值

    import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common';
    import { ApiTags, ApiOperation, ApiForbiddenResponse, ApiNotFoundResponse } from '@nestjs/swagger';
    import { AuthService } from './auth.service';
    import { LoginDto } from './dto';
    import { LocalAuthGuard } from './local-auth.guard';
    
    @ApiTags('auth')
    @Controller('auth')
    export class AuthController {
      constructor(private readonly authService: AuthService) {}
    
      @ApiOperation({ summary: 'user login' })
      @ApiForbiddenResponse({ description: 'Forbidden' })
      @ApiNotFoundResponse({ description: 'Not Found' })
      @UseGuards(LocalAuthGuard)
      @Post('/login')
      async login(@Body() _: LoginDto, @Request() req) {
        return req.user.dataValues;
      }
    }
    

    此时,我们调用登录接口,当我们输入错误的账号密码时,就会发现接口返回401的状态码,告诉我们无权限,当我们输入正取的账号密码时,就能看见返回该用户

    img

    说明我们的校验生效了,回到最开始,我们是希望登录返回一个 tokenJWT),而 token 就是包含了用户信息,过期时间等;接下来我们就实现 token

    JWT 功能

    JWT 全称是 JSON Web Token,是目前最流行的跨域认证解决方案;其原理就是服务器认证以后,生成一个 JSON 对象,包含了用户基本信息以及过期时间,然后返回给调用者,而后有权限的 API 都需要带上该信息

    安装

    $ npm install @nestjs/jwt passport-jwt
    $ npm install @types/passport-jwt --save-dev
    

    实现

    之前我们说到 LocalAuthGuard 守卫会返回 user 并以 req.user 给到请求者,现在我们就来完善生成 token

    auth.service.ts

    import { Injectable } from '@nestjs/common';
    import { JwtService } from '@nestjs/jwt';
    import { UserService } from '../user/user.service';
    
    @Injectable()
    export class AuthService {
      constructor(
        private readonly userService: UserService,
        private readonly jwtService: JwtService,
      ) {}
    
      async validateUser(username: string, pass: string): Promise<any> {
        const user = await this.userService.findOneByName(username);
        if (user && user.password === pass) {
          const { password, ...result } = user;
          return result;
        }
        return null;
      }
    
      async login(user: {username: string, id: string}) {
        const payload = { username: user.username, sub: user.id };
        return {
          access_token: `Bearer ${this.jwtService.sign(payload)}`,
        };
      }
    }
    

    @nestjs/jwt 库提供了一个 sign 函数,用于从用户对象属性的子集生成 jwt,然后返回 access_token

    接下来需要更新 auth.module.ts 来导入新的依赖项并配置 JwtModule。需要创建一个共享密钥用在 JWT 签名和验证步骤之间,为了方便,我们新建 constants.ts 来存放该密钥,但在正式的生产环境中不推荐这样做,因为密钥需要一定的保护措施,你可以放在环境变量中或者其他的方式也可以,总之要有一定的安全保护

    constants.ts

    export const jwtConstants = {
      secret: 'secretKey',
    };
    

    auth.module.ts

    import { Module } from '@nestjs/common';
    import { JwtModule } from '@nestjs/jwt';
    import { UserModule } from '../user/user.module';
    import { AuthService } from './auth.service';
    import { PassportModule } from '@nestjs/passport';
    import { LocalStrategy } from './local.strategy';
    import { jwtConstants } from './constants';
    import { AuthController } from './auth.controller';
    
    @Module({
      imports: [
        UserModule,
        PassportModule,
        PassportModule.register({ defaultStrategy: 'jwt' }),
        JwtModule.register({
          secret: jwtConstants.secret,
          signOptions: { expiresIn: '12h' },
        }),
      ],
      providers: [AuthService, LocalStrategy],
      controllers: [AuthController],
    })
    export class AuthModule {}
    

    这里我们使用 register 配置 JwtModule ,并传入一个配置对象,更多的配置项可以查看官网

    接下来修改 auth.controller.tslogin 接口,使其调用 servicelogin 方法

    @ApiTags('auth')
    @Controller('auth')
    export class AuthController {
      constructor(private readonly authService: AuthService) {}
    
      @ApiOperation({ summary: 'user login' })
      @ApiForbiddenResponse({ description: 'Forbidden' })
      @ApiNotFoundResponse({ description: 'Not Found' })
      @UseGuards(LocalAuthGuard)
      @Post('/login')
      async login(@Body() _: LoginDto, @Request() req) {
        return this.authService.login(req.user.dataValues);
      }
    }
    
    

    我们重新请求下登录接口,就能看到返回的数据就是 access_token

    img

    拦截

    接下来我们就可以使用 token 来对 API 路由进行校验;passport-jwt 策略可以用于用 JSON Web 标记保护 RESTful 端点。
    新建 jwt.strategy.ts

    import { ExtractJwt, Strategy } from 'passport-jwt';
    import { PassportStrategy } from '@nestjs/passport';
    import { Injectable } from '@nestjs/common';
    import { jwtConstants } from './constants';
    
    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
      constructor() {
        super({
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
          ignoreExpiration: false,
          secretOrKey: jwtConstants.secret,
        });
      }
    
      async validate(payload: any) {
        return { id: payload.sub, username: payload.username };
      }
    }
    
    • jwtFromRequest:提供从请求中提取 JWT 的方法。我们将使用在 API 请求的授权头中提供 token 的标准方法
    • ignoreExpiration:选择默认设置 false ,它将确保 JWT 没有过期的责任委托给 Passport 模块。这意味着,如果我们的路由提供了一个过期的 JWT ,请求将被拒绝,并发送 401 未经授权的响应
    • secretOrkey:使用权宜的选项来提供对称的密钥来签署令牌
    • validate:对于 JWT 策略,Passport 首先验证 JWT 的签名并解码 JSON 。然后调用 validate 方法,该方法将解码后的 JSON 作为其单个参数传递

    auth.module.ts 中添加新的 JwtStrategy 作为提供者

    @Module({
      imports: [
        UserModule,
        PassportModule.register({ defaultStrategy: 'jwt' }),
        JwtModule.register({
          secret: jwtConstants.secret,
          signOptions: { expiresIn: '12h' },
        }),
      ],
      providers: [AuthService, LocalStrategy, JwtStrategy],
      controllers: [AuthController],
    })
    export class AuthModule {}
    

    新建 jwt-auth.guard.ts

    import {
      ExecutionContext,
      Injectable,
      UnauthorizedException,
    } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    
    @Injectable()
    export class JwtAuthGuard extends AuthGuard('jwt') {
      canActivate(context: ExecutionContext) {
        return super.canActivate(context);
      }
    
      handleRequest(err, user) {
        if (err || !user) {
          throw err || new UnauthorizedException();
        }
        return user;
      }
    }
    

    上面我们看到 jwt.strategy.tsvalidate 方法会返回 user;在 handleRequest 我们可以看到如果找不到 user 就会抛出没有权限的错,我们在获取用户的接口加上该拦截

    user.controller.ts

    import { Controller, Get, Param, UseGuards, Post, Body } from '@nestjs/common';
    import {
      ApiTags,
      ApiOperation,
      ApiUnauthorizedResponse,
      ApiForbiddenResponse,
      ApiNotFoundResponse,
    } from '@nestjs/swagger';
    import { CreateUserDto } from './dto';
    import { UserService } from './user.service';
    import { User } from './user.model';
    import { JwtAuthGuard } from '../auth/jwt-auth.guard';
    
    @ApiTags('user')
    @Controller('user')
    export class UserController {
      constructor(private readonly userService: UserService) {}
    
      @ApiOperation({
        summary: 'find a user by id',
      })
      @ApiUnauthorizedResponse({ description: 'Unauthorized' })
      @ApiForbiddenResponse({ description: 'Forbidden' })
      @ApiNotFoundResponse({ description: 'Not Found' })
      @UseGuards(JwtAuthGuard)
      @Get(':id')
      findOne(@Param('id') id: string): Promise<User> {
        return this.userService.findOneById(id);
      }
    
      @ApiOperation({
        summary: 'find user list',
      })
      @ApiUnauthorizedResponse({ description: 'Unauthorized' })
      @ApiForbiddenResponse({ description: 'Forbidden' })
      @ApiNotFoundResponse({ description: 'Not Found' })
      @UseGuards(JwtAuthGuard)
      @Get()
      findAll(): Promise<User[]> {
        return this.userService.findAll();
      }
    
      @ApiOperation({
        summary: 'create a user',
      })
      @ApiUnauthorizedResponse({ description: 'Unauthorized' })
      @ApiForbiddenResponse({ description: 'Forbidden' })
      @ApiNotFoundResponse({ description: 'Not Found' })
      @UseGuards(JwtAuthGuard)
      @Post()
      create(
        @Body() createUserDto: CreateUserDto,
      ): Promise<User> {
        return this.userService.create(createUserDto);
      }
    }
    

    这时我们之前去访问接口,就能看见接口报 401

    img

    给文档加权限

    swagger 提供了装饰器,可以在请求头带上 token
    修改 main.ts

    import { NestFactory } from '@nestjs/core';
    import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      const options = new DocumentBuilder()
        .setTitle('nest demo example')
        .setDescription('The nest demo API description')
        .setVersion('1.0')
        .addBearerAuth()
        .build();
      const document = SwaggerModule.createDocument(app, options);
      SwaggerModule.setup('api-docs', app, document);
    
      await app.listen(3000);
    }
    bootstrap();
    

    然后在 user.controller.ts 文件里的接口加上 @ApiBearerAuth() 装饰器,这时我们看见接口问题出现了 Authorizate 图标

    img

    我们点击那个锁,就会弹窗一个弹窗让我们填写 token,我们调用登录接口,把返回回来的 access_token 值粘贴上,就能正常访问需要权限的接口了,注意由于之前返回带上了 Bearer 前缀,所以在粘贴的时候需要去掉

    img

    到此守卫就入门了,接下来期待自己深入的研究

    代码传送门:nest-demo

    参考资料:Nest 文档

    相关文章

      网友评论

          本文标题:跟随官网学nestjs之守卫

          本文链接:https://www.haomeiwen.com/subject/rdgkcltx.html