본문 바로가기

[NestJS]

[NestJS] - 8편 (JWT)

반응형

NestJS - 8편 (JWT)

인증(Authentication) & 인가(Authorization)

  • 인증(Authentication): 요청자가 자신이 누구인지 증명하는 과정
    최근에는 JWT를 이용해 많이함. 미들웨어로 구현하는것이 좋은 사례임.
  • 인가(Authorization): 인증을 통과한 유저가 기능을 사용할 권리가 있는지 판별하는 것.
    미들웨어는 실행 컨텍스트(ExecutionContext)에 접근하지 못하고 단순히 자신의 일만하고 next()를 호출하므로 다음에 어떤 핸들러가 실행될지 모르므로 가드(Guard)를 주로 이용함.

인증 실패시: 401 Unauthorized 또는 403 Forbidden 으로 응답 많이함.

인가 - 가드를 통한 인가(Authorization)

ex) 사용자 권한에 따라 접근 가능한 페이지 분리,
사용자 요금제에 따른 다른 기능 제공 등

CanActivate인터페이스 구현

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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return this.validateRequest(request);
  }

  private validateRequest(request: any) {
    return true;
  }
}

모든 Guard는 canActivate()를 구현해야한다.
현재의 request가 실행가능한 것인지 판단해 true 또는 false를 리턴한다.

인가 - 가드를 통한 권한 분리

특정 권한을 가지고 있는 유저만 접근 가능한 Guard를 구현하기.

가드 생성 ex)roles.guard.ts

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

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

가드를 적용할 컨트롤러

@Controller('user')
@UseGuards(RolesGuard)
export class UserController {}
// RoleGuard 대신 new RolesGuard() 를 사용할 수 있다.

하나의 메소드에만 적용하고싶다면 해당 메소드에 @UseGuards()데코레이터를 붙이면된다.

인가 - 글로벌 가드 & 모듈 가드

일반적으로 글로벌 가드는 이용하지 않는다.(context 밖에서 벌어지므로 의존성 주입이 불가하므로)

인가 - 글로벌 가드

main.ts파일

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

인가 - 모듈 가드

모듈

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

인가 - 권한 분리

그렇다면 권한별 분리는 어떻게 할까?

권한 데코레이터 생성

//roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

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

적용할 컨트롤러

@Post()
@Roles('admin')
async create(@Body() createUserDto: CreateUserDto) {
  this.userService.create(createUserDto);
}

인가 - 가드 생성한 곳에서 분리한 권한 적용시키기

가드 생성 ex)roles.guard.ts

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

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

인증

인증은 크게 2가지로 나눠진다. 세션 기반 인증과 토큰 기반 인증

세션

  • 세션 기반 인증 : 로그인시 서버는 따로 생성해둔 세션DB에 해당 사용자를 저장시키고 이후 사용자로부터 요청이 올 때마다 세션 DB를 체크해 저장되어있는지 확인한다.
  • 세션 인증 장점 : DB에 계속 저장중이므로 특정 사용자를 벤하거나 로그인된 다른 기기에서 다른기기의 로그아웃 등의 작업이 가능하다.
  • 세션 인증 단점 : 요청마다 DB에 체크해야하므로 느리다(그래서 Redis를 주로 이용함). 세션 DB를 따로 두어야함. DB에 저장해야하므로 특정 길이 제한을 둔다.

토큰

  • 토큰 기반 인증 : 로그인시 서버는 토큰을 생성해 보내주고 토큰을 따로 DB에 저장하지 않는다. 이후 사용자로부터 요청이 올 때마다 토큰에 대한 검증만 수행한다. JWT 를 주로 이용한다.
  • 토큰 인증 장점 : 빠르다. Facebook, Google 등 계정으로 다른 서비스 로그인이 가능한 OAuth 구현이 가능하다. 따로 DB를 둘 필요가 없다.
  • 토큰 인증 단점 : 토큰 자체가 정보이므로 탈취시 취약하다. ex) 토큰 유효기간 30분이라면 해당 사용자가 악의적인 사용자여도 내가 해당 사용자를 30분동안 벤하지 못함.

토큰이 대세이니 토큰을 알아보자!(JWT)

JWT(JSON WEB TOKEN)

  • 헤더, 페이로드, 시그니처 3가지의 구성요소를 가며 .으로 구분된다
  • 헤더와 페이로드는 base64로 인코딩되어있다.

헤더

{
    "typ":"JWT",
    "alg":"HS256"
}

typ: JWS와 JWE에 정의된 타입.
alg: 암호화 여부. 암호화 할경우 HS256등 명시. 암호화하지 않을 경우 "none"

페이로드

페이로드는 클레임(claim)이라고 불리는 정보들이다.

페이로드 - 등록된(Registered) 클레임

IANA JWT 클레임 레지스트리에 등록된 클레임이다.
필수는 아니지만 필수라고 생각하고 써야한다.(JWT 상호환성을 위해.)

  • "iss"(Issuer, 발급자): . 누가 토큰을 발급(생성)했는지를 나타냅니다. 애플리케이션에서 임의로 정의한 문자열 또는 URI 형식을 가집니다.
  • "sub" (Subject, 주제): 일반적으로 주제에 대한 설명을 나타냅니다. 토큰 주제는 발급자가 정의하는 문맥상 또는 전역으로 유일한 값을 가져야 합니다. 문자열 또는 URI 형식을 가집니다.
  • "aud" (Audience, 수신자): 누구에게 토큰이 전달되는 가를 나타냅니다. 주로 보호된 리소스의 URL을 값으로 설정합니다.
  • "exp" (Expiration, 만료 시간): 언제 토큰이 만료되는지를 나타냅니다. 만료 시간이 지난 토큰은 수락되어서는 안됩니다. 일반적으로 UNIX Epoch 시간을 사용합니다.
  • "nbf" (Not Before): 정의된 시간 이후에 토큰이 활성됩니다. 토큰이 유효해 지는 시간 이전에 미리 발급되는 경우 사용합니다. 일반적으로 UNIX Epoch 시간을 사용합니다.
  • "iat" (Issued At, 토큰 발급 시간): 언제 토큰이 발급되었는지를 나타냅니다. 일반적으로 UNIX Epoch 시간을 사용합니다.
  • "jti" (JWT ID, 토큰 식별자): 토큰의 고유 식별자로써 같은 값을 가질 확률이 없는 암호학적 방법으로 생성되어야 합니다. 공격자가 JWT를 재사용하는 것을 방지하기 위해 사용합니다.

페이로드 - 공개(Public) 클레임

JWT 발급자가 공개해도 되는 클레임이다.
일반적으로 이름 충돌을 막기위해 IANA JWT 클레임 레지스트리에 이름을 등록한다.
보통 URI형식으로 정의한다.

{
    "http://example.com/is_root": true
}

페이로드 - 비공개(Private) 클레임

JWT 발급자와 사용자간 서로 약속한 클레임이다.
이름 충돌에 주의해야한다.

시그니처는 이 토큰이 유효한 토큰인지 검사만 할 뿐 페이로드를 암호화하는것은 아니므로 비공개 클레임에 비밀번호 등 중요 정보를 절대! 넣으면 안된다.

시그니처

헤더와 페이로드를 base64로 인코딩하고 두 값을 .로 이어붙이고 헤더에서 정한 암호화 알고리즘(HS256 등)과 secret키로 암호화된 문자열이다.
시그니처는 이 토큰이 유효한 토큰인지 검사만 할 뿐 페이로드를 암호화하는것은 아니므로 비공개 클레임에 비밀번호 등 중요 정보를 절대! 넣으면 안된다.

유저 권한별 접근 기능 설정하기

https://wikidocs.net/158631

참조 : NestJS로 배우는 백엔드 프로그래밍

반응형