Guard

Guard Process

 

Guard는 '경비' 라는 뜻대로, Application을 보호하고, 유효한 사용자(authentication, 인증: 애플리케이션의 유효한 사용자인지 확인), 권한(authorization, 인가: 사용자에게 허가된 작업인지 확인)을 검증하는 역할을 한다. 검증된 사용자가 아니면 요청 자체를 막아버리는 것이다. 예를 들면, 사용자에 대한 검증이 없다면 모든 요청에 대해 응답을 할 것이고, 서버 자원에 낭비가 지속되면 서버 부하 / 다운의 결과를 초래할 가능성이 높다.

 

일반적으로 기존 Express.js 서버에서는 미들웨어(middleware)에 의해 처리되었다. 미들웨어는 인증을 위한 휼륭한 선택이지만, 토큰 검증이나 속성 연결과 같은 작업을 관리하기 힘들다.

→ 가드는 모든 미들웨어 이후에 실행되지만 인터셉터나 파이트 이전에 실행된다

 

JWT(Json Web Token)을 활용하여, 사용자 인증과 인가를 구현하는 방법을 작성하였다.

 

  1. 사용자 로그인 성공 시, AccessToken과 RefreshToken을 발급
  2. 로그인 이후 사용자는 Request Header에 AccessToken을 포함하여 요청
  3. 사용자는 AccessToken이 만료되면, 발급 받았던 RefreshToken을 통해 새로운 AccessToken을 발급

추가적으로 사용자 인증 전에는 AccessToken을 가지고 있지 않기 때문에, 로그인 로직에서는 Guard 검증을 거치지 않도록 설정

 


NPM 패키지 설치

npm i --save @nestjs/jwt

📔 src/auth/guard/public.decorator.ts

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

export const Public = () => SetMetadata('isPublic', true)

사용자 로그인 이전에는 Jwt 토큰이 없기 때문에, 함수 데코레이터 선언을 통해 로그인 API에서는 인증 가드 검증을 하지 않도록 할 예정

 

Auth Module, Service, Controller 작성

 

📔 src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UserModule } from '@src/user/user.module';

@Module({
  imports: [JwtModule.register({
    global: true
  }), UserModule],
  providers: [AuthService],
  controllers: [AuthController]
})
export class AuthModule { }

 

📔 src/auth/user.module.ts

더보기
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule { }

UserService를 exports 설정

 

 

📔 src/auth/auth.service.ts

더보기
import { Injectable, InternalServerErrorException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '@src/user/user.service';

@Injectable()
export class AuthService {

    private readonly ACCESS_TOKEN_SECRET_KEY: string;
    private readonly REFRESH_TOKEN_SECRET_KEY: string;

    constructor(private readonly jwtService: JwtService
        , private readonly userService: UserService
        , private readonly configService: ConfigService
    ) {
        this.ACCESS_TOKEN_SECRET_KEY = configService.get<string>('SERVER.ACCESS_TOKEN_SECRET_KEY')
        this.REFRESH_TOKEN_SECRET_KEY = configService.get<string>('SERVER.REFRESH_TOKEN_SECRET_KEY')
    }

    async signIn(id: string, password: string) {
        // ID를 가진 사용자가 있는지 데이터베이스 조회
        const user = await this.userService.signIn(id);

        if (!user) {
            throw new UnauthorizedException('유저 정보 없음');
        } else if (user?.password !== password) {
            throw new UnauthorizedException('비밀번호 일치하지 않음');
        }

        const payload = { id: user.id, name: user.name }

        return {
            accessToken: await this.jwtService.signAsync(payload, {
                secret: this.ACCESS_TOKEN_SECRET_KEY
                , expiresIn: '10m'
            }),
            refreshToken: await this.jwtService.signAsync(payload, {
                secret: this.REFRESH_TOKEN_SECRET_KEY
                , expiresIn: '2w'
            })
        }
    }

    async getNewAccessToken(refreshToken: string) {
        try {
            // RefreshToken 검증
            const payload = await this.jwtService.verifyAsync(
                refreshToken,
                {
                    secret: this.REFRESH_TOKEN_SECRET_KEY
                }
            )

            // 새로운 AccessToken 발급
            return {
                accessToken: await this.jwtService.signAsync({
                    id: payload.id,
                    name: payload.name
                }, {
                    secret: this.ACCESS_TOKEN_SECRET_KEY
                    , expiresIn: '10m'
                })
            }
        } catch (e) {
            if (e.name === 'TokenExpiredError') {
                throw new UnauthorizedException('refreshToken 만료');
            } else if (e.name === 'JsonWebTokenError') {
                throw new UnauthorizedException('refreshToken 정보 없음');
            }

            throw new InternalServerErrorException(e);
        }
    }

}
  • ConfigService를 통해 환경 변수 관리 (ACCESS_TOKEN_SECRET_KEY,  REFRESH_TOKEN_SECRET_KEY)
  • 토큰 만료기간은 AccessToken은 짧게, RefreshToken은 길게 설정
  • JWT 토큰의 Payload(내용)는 사용자의 ID, 이름으로 구분

 

 

 

[NestJS] 환경 변수 (config) 사용하기

환경 변수, 파일을 사용하는 이유 - 프로젝트에서 공통적으로 사용하는 변수들을 파일을 통해 쉽게 관리할 수 있다 - 코드에 secret key 값을 노출시키지 않기 때문에 보안 상 이점이 있다 - 운영(pro

clichy12.tistory.com

 

 

📔 src/auth/auth.controller.ts

더보기
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Public } from './guard/public.decorator';

@Controller('auth')
export class AuthController {

    constructor(
        private readonly authService: AuthService
    ) { }

    @HttpCode(HttpStatus.OK)
    @Post('sign-in')
    @Public()
    signIn(@Body() user: Record<string, any>) {
        return this.authService.signIn(user.id, user.password);
    }

    @HttpCode(HttpStatus.OK)
    @Post('refresh')
    @Public()
    getNewAccressToken(@Body() body: Record<string, any>) {
        return this.authService.getNewAccessToken(body.refreshToken);
    }

}

Public 데코레이터를 선언하여,  로그인이나 새로운 AccessToken을 발급 받는 API 요청 시에는 인증 가드 검증을 하지 않도록 설정

 


Auth Guard 작성

 

📔 src/auth/guard/Auth.guard.ts

import { CanActivate, ExecutionContext, Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { Request } from "express";


@Injectable()
export class AuthGuard implements CanActivate {

    private readonly ACCESS_TOKEN_SECRET_KEY: string;

    constructor(private jwtService: JwtService
         , private configService: ConfigService
         , private reflector: Reflector
        ) {
            this.ACCESS_TOKEN_SECRET_KEY = configService.get<string>('SERVER.ACCESS_TOKEN_SECRET_KEY')
        }

    async canActivate(context: ExecutionContext): Promise<boolean>{
        // Public 데코레이터를 선언한 라우터 경로는 JWT 토큰 검증을 하지 않음 (ex. 로그인)        
        const isPublic = this.reflector.get('isPublic', context.getHandler());
        if(isPublic){
            return true;
        }

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

        // Request Header Authorization. JWT AccessToken 추출
        const token = this.extractTokenFromHeader(request);
        
        if(!token) {
            throw new UnauthorizedException('accessToken 필요');
        }

        try {
            // AccessToken 검증
            const payload = await this.jwtService.verifyAsync(
                token,
                {
                    secret: this.ACCESS_TOKEN_SECRET_KEY
                }
            )

            // 라우트 핸들러에서 접근할 수 있도록 Request에 payload를 할당
            request['user'] = payload;
        } catch (e){            
            if (e.name === 'TokenExpiredError') {
                throw new UnauthorizedException('accessToken 만료');
            } else if (e.name === 'JsonWebTokenError') {
                throw new UnauthorizedException('accessToken 정보 없음');
            }
            
            throw new InternalServerErrorException(e);
        }
        
        return true;
    }

    private extractTokenFromHeader(request: Request) : string | undefined {
        const [type, token] = request.headers.authorization?.split(' ') ?? [];
        return type === 'Bearer' ? token : undefined;
    }
}
  • Public 데코레이터를 선언한 라우터 경로는 검증 하지 않음
  • Header Authorization에 있는 JWT AccessToken 검증
  • 비즈니스 로직에서 JWT 정보를 사용할 수 있도록, Request에 Payload를 할당
  • canActivate 함수에서 false를 반환하면  403 Forbidden Error를 반환
  • 토큰 만료 시, 401 Unauthorized Error를 반환하도록 설정
  @Get()
  findAll(@Req() req: Request) {
    const user = req['user'];
    console.log(user);
    
    return this.productService.findAll();
  }

 

 

📔 src/app.module.ts

	...
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from './auth/auth.module';
import { AuthGuard } from './auth/guard/Auth.guard';

@Module({
  imports: [
                ...
                AuthModule
        	],
  controllers: [
  				...
  ],
  providers: [
  				...
  			{
            provide: APP_GUARD,
            useClass: AuthGuard
		 }],
})
export class AppModule implements NestModule {
	...
}

accessToken 없이 요청 시, 401 Error 발생
로그인 성공 시, 클라이언트에게 accessToken, refreshToken을 반환
발급 받은 AccessToken 값을 요청 헤더에 포함하여 요청 &rarr; 인증 가드 검증 성공
accessToken 만료 시, 클라이언트는 발급 받은 refreshToken을 통해 새로운 accessToken을 발급

 


+ Redis (NoSQL), RDBMS 를 통해  accessToken, refreshToken 관리

+ 사용자 로그아웃 시, JWT Token 폐기 로직


참고 링크

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

 

🌐 JWT 토큰 인증 이란? (쿠키 vs 세션 vs 토큰)

Cookie / Session / Token 인증 방식 종류 보통 서버가 클라이언트 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. JWT를 배우기 앞서 우선 쿠키와 세션의 통신 방식을 복습해

inpa.tistory.com

 

 

[NestJS] Guard 사용하기

웹서버에서 중요한 것 중의 하나는 보안 관련한 로직일 것이다. NestJS에서는 허용된 User가 아니면 query나 Mutation, Subscription을 요청하지 못하도록 하는 Middleware를 제공한다. 이것을 UseGuard라는 decora

tre2man.tistory.com

 

'JavaScript > NestJS' 카테고리의 다른 글

[NestJS] Exception filters  (0) 2023.08.21
[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19

+ Recent posts