番外篇 - NestJs - Guard

NestJs - Guard

验证分为两种,登入权限验证以及角色验证
举例说明:我们将 API 分为三种情境

  • 不需要登入
    呼叫 API 时因为完全不需要验证,所以我们不会设定 Guard

  • 登入不需要权限 @UseGuards(AuthGuard)
    我们设定某些 API 是需要登入後才能呼叫的,也就是 API 的 headers 必须带着有效的 Token ,Guard 会进行 Token 解析,通过便能呼叫此 API ,如果失败就会回传 400 或者 401 的状态

  • 登入後需要角色验证 @UseGuards(AuthGuard, RolesGuard)
    最後便是角色权限的验证,像是某些 API 只能由角色是Admin 来呼叫,这时我们就会多一个 RolesGuard 的验证,@UseGuards 是有先後顺序的,我们通常会先验证 AuthGuard 再来验证 RolesGuard ,如果 RolesGuard 验证失败就会回传 403 状态

Authentication

在 NestJs 中有一个主题是在说明 Authentication,有很多种做法,我这边会针对 GraphQL 来说明如何实作 Auth 的验证

NestJs 中提供了几个套件能够验证

  • CanActivate
    @nestjs/common 提供的介面,我们需要实作此介面
  • AuthGuard
    @nestjs/passport 提供的Class,我们会透过扩充的方式来使用它

我会使用 NestJs 提供的套件 @nestjs/jwt 来产生 Token 以及解析 Token

JwtModule 的注册

@Module({
  imports: [
    JwtModule.register({
      secret: 'test', // 为了测试方便先直接明文写
      signOptions: { expiresIn: '1h' }, // 有效时长
    }),
  ]
})

在登入後使用 jwtService 产生一个有效的 Token ,里面放了 username 以及角色,方便我後续做角色验证

@Mutation(() => String)
async login (
    @Args() userArgs: UserArgs
) {
    const user = await this.userService.findUser(userArgs);
    const accessToken = this.jwtService.sign({
        role: user.role,
        username: user.username
    });    

    return accessToken;
}

将 request (req) object 传给 context

@Module({
  imports: [
    GraphQLModule.forRoot({
      context: ({ req }) => ({ req })
    }),
    UsersModule
  ]
})
export class AppModule {}

新增 Auth Guard

取得 Request headers 中的 Token ,透过在 usersService 实作好的 validateToken 来解析 Token ,将解析出来的 User 回传,最後再塞回去 req 中

import {
    ExecutionContext,
    Injectable,
    UnauthorizedException,
    BadRequestException
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';
import { UsersService } from './users.service';

@Injectable()
// export class GqlAuthGuard implements CanActivate {
export class GqlAuthGuard extends AuthGuard('jwt') {

    constructor(private readonly usersService: UsersService) { 
        super() // 实作 CanActivate 不需要
    }

    getRequest(context: ExecutionContext) {
        const ctx = GqlExecutionContext.create(context);

        return ctx.getContext().req;
    }

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const req = this.getRequest(context);
        const authHeader = req.headers.authorization as string;

        if (!authHeader) {
            throw new BadRequestException('Authorization header not found.');
        }

        const { isValid, user } = await this.usersService.validateToken(authHeader)

        if (isValid) {
            req.user = user;
            return true;
        }
        throw new UnauthorizedException('Token not valid');
    }
}

新增 Current User Decorator

能在 req 中取得刚刚放进去的 user ,再将它回传出去,我们就能够在使用 @CurrentUser 时取得 User

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const ctx = GqlExecutionContext.create(context);

    return ctx.getContext().req.user;
  },
);

接着在到需要验证的 API 上加上需要的 Decorator

@UseGuards(GqlAuthGuard)
@Query(() => TaskConnection)
async doneTasks(
    @CurrentUser() user: User,
    @Args() taskArgs: TaskArgs
) {
    const tasks = await this.taskService.queryTasks(taskArgs, TaskStatus.DONE );
    const taskCount = await this.taskService.taskCount(taskArgs, TaskStatus.DONE);

    return { tasks, taskCount};
}

如果有跨 Module 记得要将 UserService Export ,因为我们在 AuthGuard 有使用 UserService

Role-based

角色验证相对来说就简单一些

新增 Role Guard

使用 Reflector 可以取得 Decorator 的内容, super-roles 是我自定义的 Decorator ,等等会说明该如何建立

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class RolesGuard implements CanActivate {
    constructor(
        private readonly reflector: Reflector
    ) {}

    canActivate(
        context: ExecutionContext,
    ): boolean | Promise<boolean> | Observable<boolean> {
        const roles = this.reflector.get<string[]>('super-roles', context.getHandler());
        const { role } = GqlExecutionContext.create(context).getContext().req.user;

        if (roles.indexOf(role) === -1) return false;
        return true;
    }
}

新增 Super Roles

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

export const SuperRoles = (...roles: string[]) => SetMetadata('super-roles', roles);

接着到 API 上设定 Role
在 @UseGuards 上加上 RolesGuard,并使用 @SuperRoles 设定角色,RolesGuard 就能取得角色来做验证

@UseGuards(GqlAuthGuard, RolesGuard)
@SuperRoles('vip')
@Query(() => TaskConnection)
async doneTasks(
    @CurrentUser() user: User,
    @Args() taskArgs: TaskArgs
) {
    const tasks = await this.taskService.queryTasks(taskArgs, TaskStatus.DONE );
    const taskCount = await this.taskService.taskCount(taskArgs, TaskStatus.DONE);

    return { tasks, taskCount};
}

@Mutation(() => Task)
async createTask(@Args('taskData') taskData: TaskInput) {
    const task = await this.taskService.createTask(taskData);

    return task;
}

Guard 范围设定

针对单一 API

@UseGuards(RolesGuard)
@Query(() => TaskConnection)
async doneTasks() {}

针对 Resolver

@UseGuards(RolesGuard)
@Resolver()
export class TasksResolver {}

Global

main.ts

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

app.module.ts

providers: [{
    provide: APP_GUARD,
    useClass: RolesGuard,
}]

<<:  [Golang]恢复panic(recover、defer)-心智图总结

>>:  GitHub - 使用 SSH 来 push commit 吧!

[iT铁人赛Day21]疯狂程设介绍+下载

疯狂程设是一个练习CPE常用的软件,疯狂程设拥有许多练习题,不是只有CPE。 如何下载疯狂程设?可以...

[Python 爬虫这样学,一定是大拇指拉!] DAY18 - Python:Requests 基本应用 (1)

实战演练开始前,稍微来讲解一下 Requests 的基本使用,当作是暖身。 用 Requests 送...

新新新手阅读 Angular 文件 - Router - pathMatch(1) - Day27

本文内容 本文为阅读 Angular 的 Route 其中一项设定 pathMath 的笔记内容。 ...

【第十五天 - Flutter 官方 CodeLab Get-To-Know 活动报名教学(下)】

前言 我很喜欢这篇 CodeLab,我自己认为,如果这篇的内容看得懂那 Provider 基本上都会...

[Day 10] - Spring Boot 实作登入验证(四)(JWT登入验证)

今天就来完成登入验证的部分! 昨天已经完成发送帐号密码到api,验证ok即发送一笔JWT给clien...