NestJS-Guard

jaegeunsong97·2023년 11월 20일
0

NestJS

목록 보기
15/37
post-custom-banner

🖊️Guard 이론

Guard는 보호하는 역할을 합니다. API요청시, 요청 로직 처리 부분까지 갈 수 있는지 없는지를 확인해주는 것입니다.

그래서 Pipe전에 동작을 합니다.

먼저 코드를 봅시다. 현재 authorization을 넣지 않고 포스트맨을 요청하면 다음과 같은 결과가 나옵니다. 즉 서버의 어딘가에서 에러가 터진것입니다.

@Post('login/email')
postLoginEmail(
  	@Headers('authorization') rawToken: string,
) {
  	const token = this.authService.extractTokenFromHeader(rawToken, false); // 서비스 이동
  	const credentials = this.authService.decodeBasicToken(token);
  	return this.authService.loginWithEmail(credentials);
}

{
    "statusCode": 500,
    "message": "Internal server error"
}

그리고 서버 콘솔을 확인하면 다음과 같은 에러가 나와있습니다. 토큰을 주지 않았기 때문에 split을 할 수 없다는 것입니다.

즉, 이 부분에서 에러를 받은 것입니다. 하지만 서비스단까지 오지 않고 내부 진입전에 막는 방법은 없을까요? 애초에 요청단에서 막는 것이 Guard입니다.

extractTokenFromHeader(header: string, isBearer: boolean) {
    const splitToken = header.split(' '); // 에러발생
    const prefix = isBearer ? 'Bearer' : 'Basic';
    if (splitToken.length !== 2 || splitToken[0] !== prefix) throw new UnauthorizedException('잘못된 토큰입니다.');     
    const token = splitToken[1];
    return token;
}

따라서 요청단에서 막기위해 auth/guard/basic-token.guard.ts를 만들고 로직만 작성합니다다.

  • auth/guard/basic-token.guard.ts
/**
 * 구현할 기능
 * 
 * 1) 요청객체 (request)를 불러오고
 *   authorization header로부터 토큰을 가져온다.
 * 2) authService.extractTokenFromHeader를 이용해서
 *   사용 할 수 있는 형태의 토큰을 추출한다.
 * 3) authService.decodeBasicToken을 실행해서
 *   email과 password를 추출한다.
 * 4) email과 password를 이용해서 사용자를 가져온다.
 *   authService.authenticateWithEmailAndPassword
 * 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.
 *   req.user = user;
 */

🖊️BasicTokenGuard

Guard를 구현하는 것은 Pipe를 구현하는 것과 매우 비슷합니다. 따라서 제공을 받으려면 @Injectable()을 사용해서 등록을 하면됩니다.

  • auth/guard/basic-token.guard.ts
import { CanActivate, ExecutionContext } from "@nestjs/common";

@Injectable() // Provider 이기 때문에 constructor 필요
export class BasicTokenGuard implements CanActivate {
    async canActivate(context: ExecutionContext): Promise<boolean> { // boolean 이유: 통과 O X 이기 때문에
    }
}
.
.
변경
.
.
@Injectable() 
export class BasicTokenGuard implements CanActivate {
  
  	constructor(
    	private readonly authService: AuthService, // inject
    ) {}
  
    async canActivate(context: ExecutionContext): Promise<boolean> {
    }
}

이제 1개씩 추가를 해봅시다. 먼저 BasicTokenGuard를 특정 엔드포인트에 적용하면 해당 객체의 정보를 가져와야 합니다. 해당 정보를 가진 요청을 가지고 오는 방법은 스탠다드한 방법입니다.

@Injectable()
export class BasicTokenGuard implements CanActivate {

    constructor(
    	private readonly authService: AuthService,
    ) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        // switchToHTTP(Http Protocol), switchToRpc(Rpc), switchToWs(웹소켓)
        const req = context.switchToHttp().getRequest();

        // {authorization: 'Basic asdf32sadc'} -> asdf32sadc
        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; // req의 user라는 키에 가져온 user가 붙는다, 응답이 돌아갈때까지 존재
        return true;
    }
}

BasicTokenGuard를 만들었으니까 이제 사용을 합시다. auth.controller.ts로 이동하고 코드를 추가 후, 포스트맨으로 테스트를 합시다.

  • auth.controller.ts
@Post('login/email')
@UseGuards(BasicTokenGuard) // 추가
postLoginEmail(
  	@Headers('authorization') rawToken: string,
) {
    const token = this.authService.extractTokenFromHeader(rawToken, false);
    const credentials = this.authService.decodeBasicToken(token);
    return this.authService.loginWithEmail(credentials);
}

{
    "message": "토큰이 없습니다.",
    "error": "Unauthorized",
    "statusCode": 401
}

그리고 확인용으로 요청 객체를 가지고 오는 방법을 보도록 하겠습니다. @Headers('authorization') rawToken: string, 바로 밑에 @Request() req,를 추가합니다. req에 어떤 값들이 있는지 확인하겠습니다.

import { Body, Controller, Post, Headers, UseGuards, Request } from '@nestjs/common';
.
.
@Post('login/email')
@UseGuards(BasicTokenGuard)
postLoginEmail(
  	@Headers('authorization') rawToken: string,
  	@Request() req, // 추가
) {
  .
  .

{
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImNvZGVmYWN0b3J5QGNvZGVmYWN0b3J5LmFpIiwic3ViIjoxLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzA2Mjg2ODI2LCJleHAiOjE3MDYyODcxMjZ9.uLMb-ph3ockk1dsj-7ryuk04tI-qAefOd3Ag92B9xVo",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImNvZGVmYWN0b3J5QGNvZGVmYWN0b3J5LmFpIiwic3ViIjoxLCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTcwNjI4NjgyNiwiZXhwIjoxNzA2MjkwNDI2fQ.JmgdS5o-Ab_QuZYJOhr53jmn65NVUC080FE6Ff0qqmY"
}

디버깅을 해보면 토큰에 우리가 입력한 정보들이 있는 것을 알 수 있습니다.

Request는 임시로 만든것이기 때문에 지워줍니다.


🖊️BearerTokenGuard

이제 token/access와 token/refresh에서 BearerToken을 검증해야 합니다. guard 폴더에 bearer-token.guard.ts를 만듭니다.

  • bearer-token.guard.ts
@Injectable()
export class BearerTokenGuard implements CanActivate {

    constructor(
    	private readonly authService: AuthService,
    ) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const req = context.switchToHttp().getRequest();
        const rawToken = req.headers['authorization'];
        if (!rawToken) throw new UnauthorizedException('토큰이 없습니다.');
        const token = this.authService.extractTokenFromHeader(rawToken, true);
        const result = await this.authService.verifyToken(token); // 검증 -> result에는 payload

        /**
              * request에 넣을 정보
              * 
              * 1) 사용자 정보 - user
              * 2) token - token
              * 3) tokenType - access | refresh
              */
        req.token = token;
        req.tokenType = result.type;
    }
}

사용자의 정보를 넣을 려면 inject를 받아야 합니다. userService를 추가합니다.

@Injectable()
export class BearerTokenGuard implements CanActivate {

    constructor(
    	private readonly authService: AuthService,
        private readonly userService: UserService,
    ) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const req = context.switchToHttp().getRequest();
        const rawToken = req.headers['authorization'];
        if (!rawToken) throw new UnauthorizedException('토큰이 없습니다.');
        const token = this.authService.extractTokenFromHeader(rawToken, true);
        const result = await this.authService.verifyToken(token);
      
        const user = await this.userService.getUserByEmail(result.email);

        req.token = token;
        req.tokenType = result.type;
        req.user = user;
        return true;
    }
}

근데 Bearer 토큰은 access와 refresh 2개로 나뉘어지게 됩니다. 물론 로직은 동일합니다. 하지만 따로 구분해서 검증을 하고 싶은 경우가 있기 때문에 tokenType을 보고 분리하게 만듭니다.

@Injectable()
export class BearerTokenGuard implements CanActivate {

  constructor(
    private readonly authService: AuthService,
    private readonly userService: 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.authService.extractTokenFromHeader(rawToken, true);
      const result = await this.authService.verifyToken(token);
      const user = await this.userService.getUserByEmail(result.email);

      req.token = token;
      req.tokenType = result.type;
      req.user = user;
      return true;
  }
}

@Injectable()
export class AccessTokenGuard extends BearerTokenGuard {
  async canActivate(context: ExecutionContext): Promise<boolean> {
      await super.canActivate(context); // 기존의 Bearer토큰 검증 절차 + tokenType을 가져옴 
      const req = context.switchToHttp().getRequest();
      if (req.tokenType !== 'access') throw new UnauthorizedException('Access Token이 아닙니다.');
      return true;
  }
}

@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;
  }
}

이제 적용해봅시다. 사실상 BearerTokenGuard는 사용할 일이 없습니다.

  • auth.controller.ts
@Post('token/access')
@UseGuards(RefreshTokenGuard)
.
.
@Post('token/refresh')
@UseGuards(RefreshTokenGuard)
profile
블로그 이전 : https://medium.com/@jaegeunsong97
post-custom-banner

0개의 댓글