본문 바로가기

[Nest]

[Nest] [클린코드 1편] JWT 발급 및 인증 구조 깔끔하게 구현하기 (Postman API 테스트)

728x90

👋 소개

안녕하세요! 대학생 개발자 주이어입니다.

오늘은 Nest라는 새로운 카테고리로 찾아뵙게 되었습니다.

Nest는 현재 제가 프로젝트를 만들 때 가장 많이 사용하고 있는 백엔드 프레임워크 인데요.

 

원래는 Nest관련 글을 따로 분리해서 적지 않고, 프로젝트 제작 글에 조금씩 설명하는 식으로 진행했었습니다.

그런데 이번에 Nest로 여러 백엔드 아키텍쳐와 클린 코드 등 심화 부분에 대해서 공부하게 되면서 이 부분을 집중적으로 정리하고 싶어 Nest라는 카테고리를 따로 만들게 되었습니다.

 

그 첫 번째 글로 서버 보안의 기초인 JWT에 대해서 정리해보려고 합니다.

그럼 바로 본론으로 들어가도록 하겠습니다.


💡 JWT 란?

JWT(JsonWebToken)를 구현하기 전에, 먼저 JWT의 개념과 사용 목적을 이해해야 합니다.

 

JWT는 이름에서 알 수 있듯이 Token을 의미합니다.

이 Token은 클라이언트가 로그인이나 회원가입을 통해 인증에 성공했을 때 서버에서 발급해주며 클라이언트마다 고유한 Token을 가지게 됩니다.

 

이러한 JWT의 핵심 목적은 서버에 요청하는 클라이언트가 유효한 사용자인지 즉, 서버에 접근 가능한 클라이언트 인지를 확인하기 위해서 사용되는데요.

이러한 JWT는 서버 보안의 가장 기초적인 부분으로도 볼 수 있습니다.

 

요약

  • JWT는 Token을 의미한다.
  • Token은 클라이언트가 인증에 성공했을 때 서버에서 발급해준다.
  • 이후 보안이 필요한 서버 자원에 접근할 때 이 Token을 통해 사용자를 식별한다.

⚙️ JWT 발급 구현

auth Controller

JWT를 발급받기 위해서는 위에서 설명했듯이 사용자가 인증을 요청해야 하고, 그 요청을 검증해야 합니다.

그러기 위해선 해당 기능을 수행할 API가 필요하겠죠.

import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('/login')
  login(@Body() body: { username: string; password: string }) {
    return this.authService.login(body.username, body.password);
  }
}

위 코드는 auth controller 코드입니다.

login API는 사용자한테 Post 메서드로 username과 password를 전달받아 authService로 요청을 처리해달라고 전달합니다.

 

이와 같이 controller는 받은 요청을 service에 넘기고, service에서 나온 결과를 반환하는 역할을 합니다.

물론 이외에도 UseGuard와 같은 사용자 확인, DTO를 통한 데이터 검증같은 부분도 controller에서 진행하게 됩니다.

(클린코드 이후 편에서 정리할 예정입니다.)

auth Service

import { Injectable, UnauthorizedException } from '@nestjs/common';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class AuthService {
  private users = [{ id: 1, username: 'juyear', password: '1234' }];

  login(username: string, password: string) {
    const user = this.users.find(
      (u) => u.username === username && u.password === password,
    );
    if (!user) throw new UnauthorizedException('Invalid credentials');

    const token = jwt.sign(
      { id: user.id, username: user.username },
      'SECRET_KEY',
      { expiresIn: '1h' },
    );

    return { access_token: token };
  }
}

위 코드는 auth Service 코드입니다.

auth Service 코드에는 login 함수를 만들어줬으며, controller로 부터 받은 값을 통해 사용자 검증 절차를 진행합니다.

사용자 검증 절차를 진행하기 위해선 user 데이터가 필요한데, 아직 repository를 분리하지 않았고, DB도 연결하지 않았기 때문에 users라는 mockData를 만들어 주었습니다.

 

그 후 간단하게 find로 유효한 사용자인지 검증을 진행하고, 유효하다면 JWT 토큰을 발급하게 됩니다.

 

JWT 토큰을 발급하는 부분에 대해서 조금 자세히 살펴보자면,

npm install jsonwebtoken

먼저, 'jsonwebtoken' 라이브러리가 필요합니다.

해당 라이브러리는 Nest뿐만 아니라 Node.js 환경 전반에서 JWT를 발급하고 검증할 때 많이 사용되는 라이브러리 입니다.

import * as jwt from 'jsonwebtoken';

해당 라이브러리를 위와 같이 불러와줍니다.

const token = jwt.sign(
  { id: user.id, username: user.username },
  'SECRET_KEY',
  { expiresIn: '1h' },
);

그럼 위 코드와 같이 간단하게 JWT 토큰을 발급할 수 있게 됩니다.

jwt.sign에는 3가지 값이 들어가게 되는데, 아래와 같습니다.

  • Token에 저장할 사용자 식별 정보 (위 코드에서는 id와 username 정보를 넣음)
  • 유효한 Token인지 확인할 때 사용될 SECRET_KEY (실제 프로젝트에서는 고유한 KEY를 사용하셔야 합니다.)
  • Token 유효 기간 (Token이 언제 만료될지를 설정)

이렇게 필요한 값들을 넣어주셨다면, Token이 정상적으로 발급됩니다.

 

추가 설명

HEADER.PAYLOAD.SIGNATURE

JWT는 기본적으로 위와 같이 3부분으로 구성되어 있습니다.

여기서 Header는 어떤 알고리즘으로 Token이 서명되었는지를 의미하며,
PAYLOAD는 위에서 말한 3가지 값 중 첫 번째 값인 사용자 식별 정보를 의미합니다. (실제 데이터 부분)

마지막으로 SIGNATURE는 위에서 말한 3가지 값 중 두 번째 값인 SECRET_KEY를 의미합니다.


 JWT 발급 테스트 (Postman)

JWT 발급을 구현했으니 이제 실제로 잘 발급이 되는지 테스트를 진행해보도록 하겠습니다.

서버 개발을 진행할 때 프론트나 DB 등이 없다면 어떻게 테스트를 해야하는지 모르시는 분이 가끔 있는데

이럴 때는 Postman을 사용하시면 됩니다.

 

Postman은 서버 API와 같은 기능을 테스트할 때 대표적으로 사용되는 프로그램 중 하나이며, 실제 실무에서도 많이 쓰인다고 들었습니다. 무엇보다 무료로도 어느정도의 테스트를 진행할 수 있기 때문에 1인 개발자가 사용하기에도 적합합니다.

Postman 요청 테스트 사진

먼저 nest 서버를 실행시켜 준 뒤, 위와 같이 Postman으로 해당 API 주소를 입력해줍니다.

그 후 Post 요청은 Body로 데이터를 전달하니 Body를 선택해주시고, JSON으로 username과 password를 입력해줍니다.

다 입력하셨다면, 오른쪽 위에 Send 버튼을 클릭하셔서 바로 테스트를 진행할 수 있습니다.

Postman 응답 테스트 사진

요청이 완료되면 정상적으로 JWT가 발급되어 반환된 것을 확인할 수 있습니다.

무작위의 알파벳과 숫자 등이 나열되어 있는 것 같지만, 해당 Token에는 위에서 넣어주었던 3가지 값을 모두 담고 있습니다.


🔐 JWT 검증 구현

Token이 있다면 무조건 유효한 사용자일까?

Token이 있다고 해서 무조건 유효한 사용자라는 것이 보장되는건 아닙니다.

Token을 만들 때 넣었던 유효 기간이 지났을 수도 있고, 또는 누군가가 의도적으로 Token을 생성하여 서버에 요청을 보낼 수도 있습니다. 

 

그렇기에 Token이 있는 사용자가 요청을 보냈을 때, 그 Token이 유효한지도 확인을 해줘야 하는데요.

이 때 사용되는 것이 위에서 말했던 "SECRET_KEY" 입니다.

이 SECRET_KEY는 외부에 절대 공유해서는 안되며, Token을 확인할 때 사용되는 중요한 KEY 입니다.

 

그럼 이제 Token을 검증하는 코드를 한 번 구현해보도록 하겠습니다.

auth Controller

@Get('/verify')
  verify(@Headers('authorization') authHeader: string) {
    const [, token] = authHeader.split(' ');
    return this.authService.verify(token);
  }

먼저 Token 검증을 진행하기 위한 verify API를 만들어 주었습니다.

실제 프로젝트에서 Token을 검증하는 API를 만드는 경우는 거의 없지만, 테스트를 위해 간단하게 구현하였습니다.

(실제 프로젝트에서는 UseGuard, jwt.strategy 등을 사용합니다.)

 

코드에 대해서 간단히 설명해보자면, JWT는 기본적으로 Headers - authorization에 포함되어 전달되기 때문에,

Headers를 통해 Token 정보를 가져올 수 있습니다.

또한 authorization은 Bearer {토큰} 형식으로 되어있기 때문에 split을 사용하여 토큰 값만 추출할 수 있습니다.

 

그 후 실제 검증 절차는 당연히 service에서 진행하게 됩니다.

auth Service

verify(token: string) {
    try {
      return jwt.verify(token, 'SECRET_KEY');
    } catch (err) {
      throw new UnauthorizedException(`Invalid Token - ${err}`);
    }
  }

auth Service의 verify 함수 부분입니다.

controller에서 전달받은 token을 이용하여 JWT 검증을 진행합니다.

 

JWT 검증 또한 jsonwebtoken 라이브러리가 알아서 진행해주기 때문에 저희는 Token과 설정한 SECRET_KEY만 넣어주면 됩니다.

검증을 통해 유효한 Token이라는 것이 확인되면 기본적으로 Token에 포함된 PAYLOAD를 반환합니다.

이 때 PAYLOAD는 넣어줬던 3가지 값 중 사용자 식별 정보, 유효 기간 (발급 시간과 만료 시간)을 의미합니다.

 

그리고 만약 검증에 실패하게 된다면, 내부적으로 오류를 띄워 catch문으로 이동하게 됩니다.

(UnauthorizedException 오류를 사용하는 이유는 클라이언트에 어떤 오류인지 제대로 알려줄 수 있기 때문입니다.
실제 실무에서는 단순 new Error보다는 해당 상황에 맞는 


 JWT 검증 테스트 (Postman)

이제 마지막으로 Token 검증까지 테스트를 해보고 마치도록 하겠습니다.

Postman verify 요청

아까와 똑같이 Postman을 켜주시고, API 주소를 입력해줍니다. (이번엔 GET 요청입니다.)

그 후 Auth에 들어가서 Bearer Token을 선택해주시고, 오른쪽에 Token을 넣어줍니다. (아까전에 login을 통해 받은 Token)

Postman verify 응답

그럼 위와 같이 Token에 포함된 정보를 반환해주는데요.

이 정보가 위에서 말했던 사용자 식별 정보와 유효 기간 입니다.

 

사실 JWT를 사용하는 이유는 검증이 통과되는 경우보다는 통과되지 못 하는 경우를 보는게 이해가 더 잘 되겠죠.

Postman verify 검증 실패

검증이 통과되지 못 하는 경우를 보기 위해 임의로 Token을 수정하여 요청을 보내봤습니다.

그랬더니 위 사진처럼 401에러가 뜨면서 "유효하지 않은 토큰"이라는 오류 멘트가 반환되었습니다.

 

이 처럼 JWT는 보호되어야 하는 서버 자원에 대해서 인증된 사용자만 접근할 수 있도록 검증하는 역할을 합니다.


😊 마무리

이렇게 오늘 Nest로 JWT 발급 및 인증 구조에 대해서 최대한 깔끔하게 구현을 해보았습니다.

JWT 관련 개념은 실무에서 굉장히 자주 사용되는 개념으로 알고 있기 때문에, 한 번 정도 공부하시는 것을 추천드립니다.

 

JWT를 구현하는 것만으로는 클린 코드와 큰 관련이 없지만, 다음 글 부터 이번에 진행한 글과 이어서 제대로 클린 코드 부분(백엔드 아키텍쳐)에 대해서 정리해보려고 합니다.

 

현재 구상 중인 다음 글은 "UseGuard와 Decorator를 사용한 깔끔한 인증 처리" 입니다.

JWT 검증 구현 부분에서 설명드렸듯이 단순히 JWT를 검증하는 API를 제작하는 경우는 거의 없습니다.

실제로 보안이 필요한 서버 자원에서 어떻게 JWT를 이용한 인증 절차가 진행되는지에 대해서 정리하려고 합니다.

 

추가로 Decorator를 사용하여 원하는 데이터만 controller에서 사용하는 방법도 정리해보려고 합니다.

 

그럼 지금까지 읽어주셔서 감사드리며, 다음에 더 유익한 글로 찾아오도록 하겠습니다.

https://discord.gg/8Hh8WgM4zp

 

KYT CODING COMMUNITY Discord 서버에 가입하세요!

Discord에서 KYT CODING COMMUNITY 커뮤니티를 확인하세요. 25명과 어울리며 무료 음성 및 텍스트 채팅을 즐기세요.

discord.com

KYT CODING COMMUNITY 가입하기!

 
728x90