안녕하세요! 오늘은 Nest.js에서 jwt 토큰 관리하기를 주제로 글을 작성해보도록 하겠습니다. 지금까지 프론트엔드 리액트를 주제로만 글을 쓰다가 오늘 처음 백엔드를 주제로 글을 작성해보는 것 같습니다.
기존에 제가 Express.js에서 API를 개발할때, Auth Middleware를 이용하여 쉽게 토큰을 관리하였으며, 인터넷이나 친구들의 예제를 통해서 쉽게 익힐 수 있었습니다.
하지만 Nest.js에서는 토큰을 관리하는 법에 대한 예제들이 많지 않았었고, 거의 다 영어로 되어있어서 무슨말인지 이해를 못할때가 많았습니다.
그러다가 저는 토큰 관리하는법을 유튜브에서 찾아보게 되었는데, 한 영상에서 좋은 예제를 찾게되었고, 쉽게 접근 할 수 있어서 아래의 글을 작성하게 되었습니다.
클래스를 만들때, 첫번째로 Nest.js의 인터페이스인 CanActivate를 구현할것입니다. CanActivate는 토큰이 있는지 / 없는지를 판단하여 그다음 행동에 대한 제어권을 얻게 됩니다.
저는 src/middleware 폴더를 만든다음, auth.ts라는 파일을 만들어서 아래와 같이 코드를 작성해주었습니다.
import { CanActivate, ExecutionContext } from "@nestjs/common";
import HttpError from "exception/HttpError";
import { verifyToken } from "lib/token";
import getProcessEnv from 'lib/getProcessEnv';
export default class AuthGuard implements CanActivate {
public canActivate(context: ExecutionContext): boolean {
// CanActivate를 implements 하였으므로, canActivate 함수를 구현해야 합니다.
const request = context.switchToHttp().getRequest();
// 클라이언트에서 보낸 request 정보를 읽어옵니다.
const { access_token } = request.headers;
// 사용자가 헤더에 보낸 access_token key값의 토큰값.
if (access_token === undefined) {
// 토큰이 전송되지 않았다면
throw new HttpError(401, '토큰이 전송되지 않았습니다.');
}
request.user = this.validateToken(access_token);
// request.user 객체에 디코딩된 토큰(유저 정보)을 저장합니다.
return true;
}
public validateToken(token: string): string {
try {
const verify: string = jwt.verify(token, getProcessEnv('JWT_SECRET')) as string;
return verify;
} catch (error) {
switch (error.message) {
// 토큰에 대한 오류를 판단합니다.
case 'INVALID_TOKEN':
case 'TOKEN_IS_ARRAY':
case 'NO_USER':
throw new HttpError(401, '유효하지 않은 토큰입니다.');
case 'EXPIRED_TOKEN':
throw new HttpError(410, '토큰이 만료되었습니다.');
default:
throw new HttpError(500, '서버 오류입니다.');
}
}
}
}
Controller에서 AuthGuard 클래스에서 선언해주었던 request.user 객체의 정보를 읽어오려고 할때, 커스텀 데코레이터를 제작하여 유저의 정보를 읽어오도록 해보겠습니다.
저는 src/lib 폴더 경로에 user.decorator.ts 파일을 만들어서 아래의 코드를 작성했습니다.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const Token = createParamDecorator((data, ctx: ExecutionContext): ParameterDecorator => {
const request = ctx.switchToHttp().getRequest();
// 클라이언트에서 보낸 request의 정보를 가져옵니다.
// 이전에 AuthGuard 클래스에서 할당했던 request.user 객체의 정보를 return 해줍니다.
return request.user;
});
위의 createParamDecorator 함수를 이용하여 커스텀 데코레이터를 제작 할 수 있습니다.
이제 위에서 만들었던 데코레이터를 이용하여 Controller에 적용해보도록 하겠습니다.
예를 들어, 게시판 프로젝트를 만든다고 가정을 했을때 글 작성 API를 설계한다면 작성자의 정보를 데이터베이스에 적용을 해야합니다. 이때 작성자 정보는 주로 헤더로 보낸 토큰을 이용하여 주로 관리를 합니다. 그래서 글 작성 API의 컨트롤러에 데코레이터를 적용하여 테스트 해보도록 하겠습니다.
기존에 작성해놓은 Controller 코드가 있다면, 아래와 같이 수정해주세요!
import { Body, Post, Res, UseGuards } from "@nestjs/common";
@Post('/')
@UseGuards(new AuthGuard())
// useGuards 데코레이터를 이용하여 토큰 요청이 필요한 API임을 알립니다.
// UseGuards 안에는 2번 제목에서 선언했던 AuthGuard 클래스를 넣어줍니다.
public async handleCreatePost(
@Res() response: Response,
@Token() user: User,
@Body() createPostDto: PostDto
) {
console.log(user);
await this.postService.handleCreatePost(createPostDto, user);
return response.status(200).json({
status: 200,
message: '글 작성을 성공하였습니다.',
});
}
해당 데코레이터를 불러와서 console.log(user)
를 실행하여 토큰이 올바르게 전송되었는지 로그를 찍어보겠습니다. 콘솔에는 아래와 같이 잘 출력되었습니다.
이제 받아온 토큰 정보를 바탕으로 Service 파일에도 간단하게 적용해보겠습니다. 저는 TypeORM을 이용하여 데이터베이스와 연결을 해주었습니다.
public async handleCreatePost(createPostDto: PostDto, user: User): Promise<void> {
const { introduction, thumbnail,title, contents, category, postTags } = createPostDto;
const existUser = await this.userRepository.getUserById(user.githubId);
// 토큰에서 받은 githubId 객체를 이용하여 유저를 찾습니다.
const post: PostEntity = new PostEntity();
post.user = existUser;
// 유저 정보 삽입
post.title = title;
post.introduction = introduction;
post.thumbnail = thumbnail;
post.contents = contents;
post.category = category;
await this.postRepository.save(post);
}
위의 코드를 다 작성하고나서, 데이터베이스를 확인해보면 올바르게 유저 값이 들어가있는 모습이 보일 것 입니다.
사실 Nest.js에서의 토큰 관리는 제가 Nest를 사용해오면서 가장 알고싶었던 부분 중 하나였습니다. 그러다가 우연히 유튜브를 통해서 좋은 예제를 얻게되어서 앞으로의 토큰 관리를 쉽게 할 수 있을 것 같습니다.
이상으로 글을 마치도록 하겠습니다. 궁금한점이 있으시다면 댓글로 남겨주세요! 긴 글 읽어주셔서 감사합니다.
혹시 보셨다는 유튜브가 어떤 영상인지 알수있을까요??