현재 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"
}
앞으로 AccessToken을 사용해서 사용자의 정보를 가져오는 경우가 많습니다. 따라서 매번 이렇게 할 수 없고 직접 User Custom Decorator를 만들어 보겠습니다.
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()
@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"
}
User Custom Decorator의 data parameter에 대해서 알아보겠습니다.
// 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()
@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"
}