728x90
반응형
목차
이번에는 저번에 구현했던 토큰에 대해 가드를 구현하고자 한다.
https://be-senior-developer.tistory.com/279
[NestJs] 로그인 기능을 구현해 보자
목차 대략적인 로그인 구현 흐름 정리 /** * 만드려는 기능 * * 1) registerWithEmail * - email, nickname, password를 입력받고 사용자를 생성한다. * - 생성이 완료되면 accessToken과 refreshToken을 반환한다. * -
be-senior-developer.tistory.com
NestJs의 요청 라이프 사이클
여기서 질문! 토큰을 파이프로 처리하면 안되는 걸까?
파이프와 가드의 역할 차이
- 파이프(Pipe):
주로 데이터 변환 및 유효성 검사를 수행합니다. 클라이언트 요청의 데이터를 처리하기 위해 사용됩니다. 데이터의 형태를 보장하거나, 특정 조건에 맞는 데이터를 필터링합니다. - 가드(Guard):
액세스 제어를 담당합니다. 요청이 처리되기 전에 특정 조건(예: 인증, 권한)이 충족되는지 확인하고, 조건을 충족하지 않으면 요청을 막습니다.
토큰을 파이프로 검증하면 안 되는 이유
- 토큰은 인증 및 권한 제어의 핵심 요소임
토큰은 사용자 인증(누구인지 확인)과 권한(무엇을 할 수 있는지 결정)을 위한 중요한 요소입니다. 인증 및 권한 처리는 액세스 제어의 핵심 작업이므로, 파이프 대신 가드에서 처리하는 것이 맞습니다. - 파이프는 데이터를 변환하거나 형식을 검증하는 데 적합
파이프는 단순한 데이터 유효성 검사 및 변환을 위한 도구입니다. 토큰의 복호화나 유효성 검증은 단순 데이터 확인 이상의 작업(예: 서명 검증, 유효기간 확인 등)이 필요하므로, 파이프로 처리하기에는 부적합합니다. - 가드는 요청을 아예 차단할 수 있음
가드는 조건을 충족하지 않을 경우 요청을 애초에 처리하지 않고, 서버에서 즉시 거부합니다. 반면, 파이프는 데이터를 변환하거나 검증한 후 다음 단계로 데이터를 전달하므로, 부적합한 토큰을 처리하려는 위험이 있습니다. - 토큰 검증은 보안상 중요한 작업임
토큰 검증은 민감한 보안 작업으로, 전문적으로 설계된 인증 라이브러리나 가드(예: AuthGuard)를 사용하는 것이 더 안전합니다. 파이프로 검증하면 실수로 토큰 처리 로직을 누락하거나 잘못된 상태로 데이터를 전달할 위험이 있습니다.
따라서 우리는 여기서 guard를 구현해 볼 것이다.
가드역할은 다음과 같다.
우리는 여기서 authorization을 넣지 않았을 때 함수가 실행되게 하고 싶지 않다.
하지만 다음을 보면 함수가 실행된 것을 볼 수 있다.
Basic Token 가드 구현하기
/**
* 구현할 기능
*
* 1) 요청객체 (request)를 불러오고
* authorization header로부터 토큰을 가져온다.
* 2) authService.extractTokenFromHeader를 이용해서
* 사용할 수 있는 형태의 토큰을 추출한다.
* 3) authService.decodeBasicToken을 실행해서
* email과 password를 추출한다.
* 4) email과 password를 이용해서 사용자를 가져온다.
* authService.authenticatedWithEmailAndPassword
* 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.
* req.user = user;
*/
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from '../auth.service';
@Injectable()
export class BasicTokenGuard implements CanActivate {
constructor(private readonly authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
// {authorization: 'Basic fsfsfsfsfsf'}
//sfsfsfsfsfsf
const rawToken = req.headers.authorization;
if (!rawToken) {
throw new UnauthorizedException('토큰이 없습니다!');
}
const token = this.authService.extractTokenFromHeader(rawToken, false);
const { email, password } = this.authService.decodeBasicToken(token);
const user = await this.authService.authenticatedWithEmailAndPassword({
email,
password,
});
req.user = user;
return true;
}
}
Bearer 토큰 가드 구현하기
/**
* 구현할 기능
*
* 1) 요청객체 (request)를 불러오고
* authorization header로부터 토큰을 가져온다.
* 2) authService.extractTokenFromHeader를 이용해서
* 사용할 수 있는 형태의 토큰을 추출한다.
* 3) authService.decodeBasicToken을 실행해서
* email과 password를 추출한다.
* 4) email과 password를 이용해서 사용자를 가져온다.
* authService.authenticatedWithEmailAndPassword
* 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.
* req.user = user;
*/
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from '../auth.service';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class BearerTokenGuard implements CanActivate {
constructor(
private readonly authService: AuthService,
private readonly userService: UsersService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
// {authorization: 'Basic fsfsfsfsfsf'}
//sfsfsfsfsfsf
const rawToken = req.headers.authorization;
if (!rawToken) {
throw new UnauthorizedException('토큰이 없습니다!');
}
const token = this.authService.extractTokenFromHeader(rawToken, true);
const result = await this.authService.verifyToken(token);
/**
* request에 넣을 정보
*
* 1) 사용자 정보
* 2) token - token
* 3) tokenType - access | refresh
*
*/
const user = await this.userService.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;
}
}
@Injectable()
export class RefreshTokenGuard extends BearerTokenGuard {
async canActivate(context: ExecutionContext): Promise<boolean> {
await super.canActivate(context);
const req = context.switchToHttp().getRequest();
if (req.tokenType !== 'refresh') {
throw new UnauthorizedException('Refresh Token이 아닙니다.');
}
return true;
}
}
커스텀 데코레이터 만들기
우리는 토큰 가드를 통과할 때 인자를 쉽게 반환하여 쓰고 싶다.
그럴때 커스텀 데코레이터를 사용해서 원하는 값을 추출해 주면 된다.
user.decorator.ts
import {
createParamDecorator,
ExecutionContext,
InternalServerErrorException,
} from '@nestjs/common';
export const User = createParamDecorator((data, context: ExecutionContext) => {
const req = context.switchToHttp().getRequest();
const user = req.user;
if (!user) {
throw new InternalServerErrorException(
'User 데코레이터는 AccessTokenGuard와 함께 사용해야 합니다. Request에 user 프로퍼티가 존재하지 않습니다!',
);
}
return user;
});
이렇게 만든 토큰 가드들을 이제 사용해야 한다.
posts.controller.ts
@Post()
@UseGuards(AccessTokenGuard)
postPosts(
@User() user: UsersModel,
@Body('title') title: string,
@Body('content') content: string,
) {
const authorId = user.id;
return this.postsService.createPost(authorId, title, content);
}
반응형
'코딩 정보 > NestJs' 카테고리의 다른 글
[NestJs] DTO란 무엇이고 사용방법에 대해 알아보자 (0) | 2025.01.27 |
---|---|
[NestJs] 포스트맨 로그인 간편화 해보자 (0) | 2025.01.27 |
[NestJs] 파이프에 대해 알아보자 (0) | 2025.01.21 |
[NestJs] 로그인 기능을 구현해 보자 (0) | 2025.01.21 |
[NestJs][typeorm] 기본적인 관계형 database를 만들어 보자 (0) | 2025.01.12 |