NestJS-Custom Decorator(커스텀 데코레이터)

jaegeunsong97·2023년 11월 25일
0

NestJS

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

🖊️AccessTokenGuard

현재 post 컨트롤러에는 큰 문제가 있습니다. 아무 사용자나 다른 사람의 post를 생성할 수 있습니다.

@Post()
postPosts(
    @Body('authorId') authorId: number, // 여기 때문에
    @Body('title') title: string,
    @Body('content') content: string,
) {
    return this.postsService.createPost(
      	authorId, title, content
    );
}

그래서 이때 Private Route라고 하는데, 로그인을 한 사용자만 사용을 할 수 있도록 만듭니다.

@Post()
@UseGuards(AccessTokenGuard) // 여기에 사용자의 id가 있다.
postPosts(
    @Body('authorId') authorId: number,
    @Body('title') title: string, 
    @Body('content') content: string,
) {
    const authorId = req.user.id;
    return this.postsService.createPost(authorId, title, content);
}

AccessToken을 받으면서 3개의 Body를 받는 것을 포스트맨으로 테스트해보겠습니다. 토큰을 넣지 않고 보내면 다음과 같이 나오게 됩니다.

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

요번에는 Bearer {accessToken}을 넣어서 테스트해봅시다.

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

authorId가 null이 들어왔다는 에러입니다. 그래서 authorId 대신에 Request에서 사용자 정보를 가져오도록 하겠습니다. AccesTokenGuard를 들어가보면 req.user = user라고 하는 코드가 있고, 이곳에 로그인한 사용자의 정보가 들어있습니다.

import { Body, Controller, Delete, Get, NotFoundException, Param, ParseIntPipe, Post, Put, UseGuards, Request } from '@nestjs/common';
.
.
@Post()
@UseGuards(AccessTokenGuard) // req.user = user
postPosts(
    @Request() req: any,
    @Body('title') title: string, 
    @Body('content') content: string,
) {
    const authorId = req.user.id; // 사용자의 id
    return this.postsService.createPost(authorId, title, content);
}

포스트맨으로 테스트를 하기에 앞서 왜 req에 user의 정보가 있는지 다시 확인하겠습니다. 이전에 Guard를 구현할 때 AccessTokenGuard에는 사용자 정보를 받는 코드가 있었습니다.

@Injectable()
export class BearerTokenGuard implements CanActivate {

    constructor(
      	private readonly authService: AuthService,
    	private readonly usersService: 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.usersService.getUserByEmail(result.email);

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

@Injectable()
export class AccessTokenGuard extends BearerTokenGuard {

     async canActivate(context: ExecutionContext): Promise<boolean> {
          await super.canActivate(context);
          const req = context.switchToHttp().getRequest();
          if (req.tokenType !== 'access') throw new UnauthorizedException('Access Token이 없습니다.');
          return true;
     }
}

AccessTokenGuard는 BearerTokenGuard를 상속하기 때문에 request에는 사용자의 담는 부분이 있습니다. req.user = user;

따라서 post 컨트롤러에서 @Body('authorId') authorId: number 이렇게 받으면 누구나 접근이 가능하기 때문에 Guard로 막고 Guard의 정보가 authorId이기 때문에 적용을 하는 것입니다.

테스트를 해보겠습니다.

{
    "title": "첫번째 title",
    "content": "첫번쨰 content",
    "likeCount": 0,
    "commentCount": 0,
    "author": {
        "id": 1
    },
    "id": 2,
    "updatedAt": "2024-01-27T17:40:51.930Z",
    "createdAt": "2024-01-27T17:40:51.930Z"
}

🖊️User Custom Decorator

앞으로 AccessToken을 사용해서 사용자의 정보를 가져오는 경우가 많습니다. 따라서 매번 이렇게 할 수 없고 직접 User Custom Decorator를 만들어 보겠습니다.

  • user/decorator/user.decorator.ts
import { ExecutionContext, InternalServerErrorException, createParamDecorator } from "@nestjs/common";
import { UsersModel } from "../entities/users.entity";

export const User = createParamDecorator((data, context: ExecutionContext) => {
     const req = context.switchToHttp().getRequest();
     const user = req.user;
     if (!user) throw new InternalServerErrorException('User 데코레이터는 AccessTokenGuard와 함께 사용해야합니다.'); // user 데코레이터를 accessToken과 함께 사용하지 않으면 발생
     return user; // User 데코레이터를 parameter에 사용했을 때, parameter의 arg 값 return
});

request 전체가 필요한게 아니고 사용자 정보가 필요한 것이니까 User라는 커스텀 데코레이터를 직접 만들어서 사용자 정보만 받도록 만드는 것입니다. 이제 컨트롤러에 추가해봅시다.

  • post.controller.ts
@Post()
@UseGuards(AccessTokenGuard)
postPosts(
    @Request() req: any, // <- @Body('authorId') authorId: number,
    @Body('title') title: string,
    @Body('content') content: string,
) {
      const authorId = req.user.id;
      return this.postsService.createPost(
        	authorId, title, content
      );
}
.
.
변경
.
.
@Post()
@UseGuards(AccessTokenGuard)
postPosts(
    @Request() user: UsersModel // 변경
    @Body('title') title: string,
    @Body('content') content: string,
) {
      const authorId = user.id; // 변경
      return this.postsService.createPost(
        	authorId, title, content
      );
}

중요한 것은 AccessTokenGuard를 통과를 했을 때 User Custom Decorator를 사용할 수 있다. 왜냐하면 요청 객체에 사용자의 정보가 있어야하기 때문이다.

// JWT 오류 잡기
- auth.service.ts

verifyToken(token: string) {
	// verify는 jwt 패키지에 존재
	return this.jwtService.verify(token, { 
		secret: JWT_SECRET,
	}); 
}
.
.
변경
.
.
verifyToken(token: string) {
	try {
        // verify는 jwt 패키지에 존재
            return this.jwtService.verify(token, { 
            secret: JWT_SECRET,
        }); 
    } catch (error) {
    	throw new UnauthorizedException('토큰이 만료됐거나 잘못된 토큰입니다. ');
    }
}

포스트맨 테스트를 해보겠습니다.

{
    "title": "첫번째 title",
    "content": "첫번쨰 content",
    "likeCount": 0,
    "commentCount": 0,
    "author": {
        "id": 1
    },
    "id": 3,
    "updatedAt": "2024-01-27T18:00:05.730Z",
    "createdAt": "2024-01-27T18:00:05.730Z"
}

🖊️Custom Decorator의 data parameter

User Custom Decorator의 data parameter에 대해서 알아보겠습니다.

  • user/decorator/user.decorator.ts
// data: key of UsersModel | undefined: data에는 UsersModel의 키 값만 가능 또는 undefined으로만 받을 것이다.
export const User = createParamDecorator((data: keyof UsersModel | undefined, context: ExecutionContext) => {
     const req = context.switchToHttp().getRequest();
     const user = req.user as UsersModel; // '사용자 모델이다' 라는 의미
     if (!user) throw new InternalServerErrorException('User 데코레이터는 AccessTokenGuard와 함께 사용해야합니다.');
     if (data) return user[data]; 
     return user;
});

data를 받을 때, data는 UsersModel의 key값만 받거나 undefined형태만 받습니다. 따라서 받아온 값은 key값이 있거나 undefined형태가 되고 유효성 체크 부분 if (data) return user[data];에서 걸러지게 됩니다.

  • post.controller.ts
@Post()
@UseGuards(AccessTokenGuard)
postPosts(
    @Request() req: any, // <- @Body('authorId') authorId: number,
    @Body('title') title: string,
    @Body('content') content: string,
) {
    const authorId = req.user.id;
    return this.postsService.createPost(
      	authorId, title, content
    );
}
.
.
변경
.
.
@Post()
@UseGuards(AccessTokenGuard) 
postPosts(
    @User('id') userId: number, // 변경
    @Body('title') title: string, 
    @Body('content') content: string,
) {
    return this.postsService.createPost(userId, title, content); // 변경
}

포스트맨 테스트를 합니다.

{
    "title": "첫번째 title",
    "content": "첫번쨰 content",
    "likeCount": 0,
    "commentCount": 0,
    "author": {
        "id": 1
    },
    "id": 6,
    "updatedAt": "2024-01-27T18:11:07.035Z",
    "createdAt": "2024-01-27T18:11:07.035Z"
}
profile
블로그 이전 : https://medium.com/@jaegeunsong97
post-custom-banner

0개의 댓글