본문 바로가기
NestJS

NestJS - Guard

by Programmer.Junny 2025. 2. 27.

1. Guard 란?

Guard는 요청이 컨트롤러의 핸들러에 도달하기 전에, 인증(Authentication)이나 인가(Authorization) 같은 추가적인 보안 로직을 수행하는 데 사용된다. Guard는 특정 조건이 충족되지 않으면 요청 처리를 중단하고, 그에 따른 예외를 던져 클라이언트에게 접근을 거부할 수 있다.

 

정리하자면 Pipe 보다는 앞에서 실행되는 로직들이고, 주로 인증이나 인가의 검증 등에 사용된다.

2. BasicTokenGuard

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "../auth.service";

@Injectable()
export class BasicTokenGuard implements CanActivate {
    constructor(private readonly authService: AuthService) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const req = context.switchToHttp().getRequest();

        // { authorization: Basic asdfaldsfadlskfj }
        const rawToken = req.headers['authorization'];

        if(!rawToken) {
            throw new UnauthorizedException('토큰이 없습니다!');
        }

        const token = this.authService.extractTokenFromHeader(rawToken, false);

        const { email, password } = this.authService.decodeBasicToken(token);

        const user = await this.authService.authenticateWithEmailAndPassword({
            email,
            password,
        });

        req.user = user;

        return true;
    }
}

Guard는 CanActivateimplements 하여야하며, 이런 경우 canActivate 메서드를 구현하여야 한다.

const req = context.switchToHttp().getRequest();

이 부분은 요청 객체를 가져온다.

const token = this.authService.extractTokenFromHeader(rawToken, false);

authService에 있는 로직으로 'Basic [토큰]'  에서 토큰 부분만을 가져온다.

Basic은 토큰의 생성에 관한 부분이기 때문에, 토큰 부분은 email:password를 base64로 인코딩한 것이라고 유추할 수 있다.

const { email, password } = this.authService.decodeBasicToken(token);

authService에서 decodedBasicToken을 호출하여 인코딩된 토큰을 디코딩하여 email과 password로 가져온다.

const user = await this.authService.authenticateWithEmailAndPassword({
            email,
            password,
        });

마지막으로 authServiceauthenticateWithEmailAndPassword 를 호출하여, 사용자가 존재하는지, 비밀번호가 일치하는지 등을 체크한다.

문제가 없으면 true를 반환한다.

@Post('login/email')
  @UseGuards(BasicTokenGuard)
  postLoginEmail(
    @Headers('authorization') rawToken: string,
  )

auth.controller.tspostLoginEmail에 적용한다. postLoginEmail은 사용자가 이메일과 비밀번호를 입력하여 로그인하는 API이다.

3. BearerTokenGuard

Bearer는 토큰의 갱신에서 사용하는 헤더이다. 클라이언트에서 'Bearer [토큰]' 의 형식으로 'Authorization' 헤더를 보내게 되면 서버에서는 헤더를 받아 토큰의 갱신을 진행한다. 

토큰 갱신을 진행하기 전에 Guard에서 토큰의 검증 등을 진행하도록 구현한다.

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "../auth.service";
import { UsersService } from "src/users/users.service";

@Injectable()
export class BearerTokenGuard implements CanActivate {
    constructor(
        private readonly authSurvice: AuthService,
        private readonly usersService: UsersService,
    ) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const req = context.switchToHttp().getRequest();

        const rawToken = req.headers['authorization'];

        if(!rawToken) {
            throw new UnauthorizedException('토큰이 없습니다!');
        }

        const token = this.authSurvice.extractTokenFromHeader(rawToken, true);

        const result = await this.authSurvice.verifyToken(token);

        /**
         * request에 넣을 정보

         * 1) 사용자 정보 - user
         * 2) token - token
         * 3) tokenType - access | refresh
         */
        const user = await this.usersService.getUserByEmail(result.email);

        req.user = user;
        req.token = token;
        req.tokenType = result.type;

        return true;
    }
}

BasicTokenGuard 와 마찬가지로 CanActivateimplements 한 후 canActivate를 구현한다.

constructor(
        private readonly authSurvice: AuthService,
        private readonly usersService: UsersService,
    ) {}

생성자로는 AuthService와 UsersService를 주입한다.

const req = context.switchToHttp().getRequest();

const rawToken = req.headers['authorization'];

req 객체(요청 객체)를 가져온 다음 authorization 헤더만을 가져온다.

const token = this.authSurvice.extractTokenFromHeader(rawToken, true);

const result = await this.authSurvice.verifyToken(token);

extractTokenFromHeader는 토큰 부분만을 뽑아오는 로직이다. Bearer는 토큰의 갱신이기 때문에 토큰에 해당하는 부분이 AccessToken, RefreshToken이 온다.

이 토큰을 authSurvice에서 구현해둔 verifyToken에 넣어 토큰을 검증한다. result는 토큰을 검증한 후 토큰의 Payload가 반환된다.

const user = await this.usersService.getUserByEmail(result.email);

req.user = user;
req.token = token;
req.tokenType = result.type;

return true;

userService에 getUserByEmail을 호출하여 email로 사용자를 가져온다.

이후 req.user, req.token, req.tokenType 등을 넣어준다.

여기서 req.user와 같은 것들은 임의의 새로운 객체를 생성하여 Request에 넣어주는 것이다. 그러면 응답하기 전까지는 다른 메서드에서도 req에 저장된 객체들을 사용할 수 있다.

3.1. AccessTokenGuard

@Injectable()
export class AccessTokenGuard extends BearerTokenGuard {
    async canActivate(context: ExecutionContext): Promise<boolean> {
        await super.canActivate(context);

        const req = context.switchToHttp().getRequest();

        if(req.tokenType !== 'access') {
            throw new UnauthorizedException('Access Token이 아닙니다.');
        }

        return true;
    }
}

그렇게 생성된 BearerTokenGuard를 가지고 AccessTokenGuard를 만들 수 있다.

await super.canActivate(context);

BearerTokenGuard 의 canActivate를 실행한다.

const req = context.switchToHttp().getRequest();

if(req.tokenType !== 'refresh') {
    throw new UnauthorizedException('Refresh Token이 아닙니다.');
}

req.tokenType을 이미 BearerTokenGuard에서 저장했으므로 사용할 수 있게 된다. 이것이 access인지 체크한다.

3.2. RefreshTokenGuard

@Injectable()
export class RefreshTokenGuard extends BearerTokenGuard {
    async canActivate(context: ExecutionContext): Promise<boolean> {
        await super.canActivate(context);

        const req = context.switchToHttp().getRequest();

        if(req.tokenType !== 'refresh') {
            throw new UnauthorizedException('Refresh Token이 아닙니다.');
        }

        return true;
    }
}

3.3. 적용하기

@Post('token/access')
  @UseGuards(RefreshTokenGuard)
  postTokenAccess(
    @Headers('authorization') rawToken: string,
  )
@Post('token/refresh')
  @UseGuards(RefreshTokenGuard)
  postTokenRefresh(
    @Headers('authorization') rawToken: string,
  )

위와 같이 AccessToken, RefreshToken 발급에 @UserGuards(RefreshTokenGuard)를 하여 적용한다.

RefreshTokenGuard만 쓰인 이유는 AccessToken이나 RefreshToken이나 전부 기존 RefreshToken을 기반으로 재발급이 되기 때문이다.

'NestJS' 카테고리의 다른 글

Postman 심화기능  (0) 2025.02.27
NestJS - Custom Decorator  (0) 2025.02.27
NestJS - Pipe  (0) 2025.02.26
VSC 디버거 사용하기  (0) 2025.02.26
Authentication - 로직 구현  (0) 2025.02.26

최근댓글

최근글

skin by © 2024 ttuttak