iT邦幫忙

第 12 屆 iThome 鐵人賽

0
Modern Web

NestJs 讀書筆記系列 第 31

番外篇 - NestJs - Guard

  • 分享至 

  • xImage
  •  

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,
}]

上一篇
總結
系列文
NestJs 讀書筆記31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言