Guard
Guard는 '경비' 라는 뜻대로, Application을 보호하고, 유효한 사용자(authentication, 인증: 애플리케이션의 유효한 사용자인지 확인), 권한(authorization, 인가: 사용자에게 허가된 작업인지 확인)을 검증하는 역할을 한다. 검증된 사용자가 아니면 요청 자체를 막아버리는 것이다. 예를 들면, 사용자에 대한 검증이 없다면 모든 요청에 대해 응답을 할 것이고, 서버 자원에 낭비가 지속되면 서버 부하 / 다운의 결과를 초래할 가능성이 높다.
일반적으로 기존 Express.js 서버에서는 미들웨어(middleware)에 의해 처리되었다. 미들웨어는 인증을 위한 휼륭한 선택이지만, 토큰 검증이나 속성 연결과 같은 작업을 관리하기 힘들다.
→ 가드는 모든 미들웨어 이후에 실행되지만 인터셉터나 파이트 이전에 실행된다
JWT(Json Web Token)을 활용하여, 사용자 인증과 인가를 구현하는 방법을 작성하였다.
- 사용자 로그인 성공 시, AccessToken과 RefreshToken을 발급
- 로그인 이후 사용자는 Request Header에 AccessToken을 포함하여 요청
- 사용자는 AccessToken이 만료되면, 발급 받았던 RefreshToken을 통해 새로운 AccessToken을 발급
추가적으로 사용자 인증 전에는 AccessToken을 가지고 있지 않기 때문에, 로그인 로직에서는 Guard 검증을 거치지 않도록 설정
NPM 패키지 설치
npm i --save @nestjs/jwt
📔 src/auth/guard/public.decorator.ts
import { SetMetadata } from "@nestjs/common";
export const Public = () => SetMetadata('isPublic', true)
사용자 로그인 이전에는 Jwt 토큰이 없기 때문에, 함수 데코레이터 선언을 통해 로그인 API에서는 인증 가드 검증을 하지 않도록 할 예정
Auth Module, Service, Controller 작성
📔 src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UserModule } from '@src/user/user.module';
@Module({
imports: [JwtModule.register({
global: true
}), UserModule],
providers: [AuthService],
controllers: [AuthController]
})
export class AuthModule { }
📔 src/auth/user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
exports: [UserService]
})
export class UserModule { }
UserService를 exports 설정
📔 src/auth/auth.service.ts
import { Injectable, InternalServerErrorException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '@src/user/user.service';
@Injectable()
export class AuthService {
private readonly ACCESS_TOKEN_SECRET_KEY: string;
private readonly REFRESH_TOKEN_SECRET_KEY: string;
constructor(private readonly jwtService: JwtService
, private readonly userService: UserService
, private readonly configService: ConfigService
) {
this.ACCESS_TOKEN_SECRET_KEY = configService.get<string>('SERVER.ACCESS_TOKEN_SECRET_KEY')
this.REFRESH_TOKEN_SECRET_KEY = configService.get<string>('SERVER.REFRESH_TOKEN_SECRET_KEY')
}
async signIn(id: string, password: string) {
// ID를 가진 사용자가 있는지 데이터베이스 조회
const user = await this.userService.signIn(id);
if (!user) {
throw new UnauthorizedException('유저 정보 없음');
} else if (user?.password !== password) {
throw new UnauthorizedException('비밀번호 일치하지 않음');
}
const payload = { id: user.id, name: user.name }
return {
accessToken: await this.jwtService.signAsync(payload, {
secret: this.ACCESS_TOKEN_SECRET_KEY
, expiresIn: '10m'
}),
refreshToken: await this.jwtService.signAsync(payload, {
secret: this.REFRESH_TOKEN_SECRET_KEY
, expiresIn: '2w'
})
}
}
async getNewAccessToken(refreshToken: string) {
try {
// RefreshToken 검증
const payload = await this.jwtService.verifyAsync(
refreshToken,
{
secret: this.REFRESH_TOKEN_SECRET_KEY
}
)
// 새로운 AccessToken 발급
return {
accessToken: await this.jwtService.signAsync({
id: payload.id,
name: payload.name
}, {
secret: this.ACCESS_TOKEN_SECRET_KEY
, expiresIn: '10m'
})
}
} catch (e) {
if (e.name === 'TokenExpiredError') {
throw new UnauthorizedException('refreshToken 만료');
} else if (e.name === 'JsonWebTokenError') {
throw new UnauthorizedException('refreshToken 정보 없음');
}
throw new InternalServerErrorException(e);
}
}
}
- ConfigService를 통해 환경 변수 관리 (ACCESS_TOKEN_SECRET_KEY, REFRESH_TOKEN_SECRET_KEY)
- 토큰 만료기간은 AccessToken은 짧게, RefreshToken은 길게 설정
- JWT 토큰의 Payload(내용)는 사용자의 ID, 이름으로 구분
📔 src/auth/auth.controller.ts
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Public } from './guard/public.decorator';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService
) { }
@HttpCode(HttpStatus.OK)
@Post('sign-in')
@Public()
signIn(@Body() user: Record<string, any>) {
return this.authService.signIn(user.id, user.password);
}
@HttpCode(HttpStatus.OK)
@Post('refresh')
@Public()
getNewAccressToken(@Body() body: Record<string, any>) {
return this.authService.getNewAccessToken(body.refreshToken);
}
}
Public 데코레이터를 선언하여, 로그인이나 새로운 AccessToken을 발급 받는 API 요청 시에는 인증 가드 검증을 하지 않도록 설정
Auth Guard 작성
📔 src/auth/guard/Auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { Request } from "express";
@Injectable()
export class AuthGuard implements CanActivate {
private readonly ACCESS_TOKEN_SECRET_KEY: string;
constructor(private jwtService: JwtService
, private configService: ConfigService
, private reflector: Reflector
) {
this.ACCESS_TOKEN_SECRET_KEY = configService.get<string>('SERVER.ACCESS_TOKEN_SECRET_KEY')
}
async canActivate(context: ExecutionContext): Promise<boolean>{
// Public 데코레이터를 선언한 라우터 경로는 JWT 토큰 검증을 하지 않음 (ex. 로그인)
const isPublic = this.reflector.get('isPublic', context.getHandler());
if(isPublic){
return true;
}
const request = context.switchToHttp().getRequest();
// Request Header Authorization. JWT AccessToken 추출
const token = this.extractTokenFromHeader(request);
if(!token) {
throw new UnauthorizedException('accessToken 필요');
}
try {
// AccessToken 검증
const payload = await this.jwtService.verifyAsync(
token,
{
secret: this.ACCESS_TOKEN_SECRET_KEY
}
)
// 라우트 핸들러에서 접근할 수 있도록 Request에 payload를 할당
request['user'] = payload;
} catch (e){
if (e.name === 'TokenExpiredError') {
throw new UnauthorizedException('accessToken 만료');
} else if (e.name === 'JsonWebTokenError') {
throw new UnauthorizedException('accessToken 정보 없음');
}
throw new InternalServerErrorException(e);
}
return true;
}
private extractTokenFromHeader(request: Request) : string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
- Public 데코레이터를 선언한 라우터 경로는 검증 하지 않음
- Header Authorization에 있는 JWT AccessToken 검증
- 비즈니스 로직에서 JWT 정보를 사용할 수 있도록, Request에 Payload를 할당
- canActivate 함수에서 false를 반환하면 403 Forbidden Error를 반환
- 토큰 만료 시, 401 Unauthorized Error를 반환하도록 설정
@Get()
findAll(@Req() req: Request) {
const user = req['user'];
console.log(user);
return this.productService.findAll();
}
📔 src/app.module.ts
...
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from './auth/auth.module';
import { AuthGuard } from './auth/guard/Auth.guard';
@Module({
imports: [
...
AuthModule
],
controllers: [
...
],
providers: [
...
{
provide: APP_GUARD,
useClass: AuthGuard
}],
})
export class AppModule implements NestModule {
...
}
+ Redis (NoSQL), RDBMS 를 통해 accessToken, refreshToken 관리
+ 사용자 로그아웃 시, JWT Token 폐기 로직
참고 링크
'JavaScript > NestJS' 카테고리의 다른 글
[NestJS] Exception filters (0) | 2023.08.21 |
---|---|
[NestJS] Swagger 적용하기 (0) | 2023.08.21 |
[NestJS] TypeORM CRUD 생성하기 (User) (0) | 2023.08.21 |
[NestJS] 환경 변수 (config) 사용하기 (0) | 2023.08.20 |
[NestJS] Logging 하기 (0) | 2023.08.19 |