본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
인증이란 특정 사용자가 인증된 사용자인지 확인하는 절차를 의미
Nest.js에서는 JWT를 통한 인증을 사용함
npm g resource user
import { IsString } from 'class-validator';
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
} from 'typeorm';
@Entity({
name: 'users',
})
export class User {
@PrimaryGeneratedColumn()
id: number;
@IsString()
@Column('varchar', { length: 10, nullable: false })
userId: string;
@IsString()
@Column('varchar', { length: 10, select: false, nullable: false })
password: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt?: Date;
}
import { PickType } from '@nestjs/mapped-types';
import { User } from '../entities/user.entity';
export class CreateUserDto extends PickType(User, [
'userId',
'password',
] as const) {}
@nestjs/jwt
설치npm i @nestjs/jwt
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
imports: [
// 현재 모듈에서 사용할 엔티티를 설정
TypeOrmModule.forFeature([User]),
// JwtModule이라는 동적 모듈을 설정하고 다른 user 모듈에서 사용하기 위한 코드
JwtModule.registerAsync({
useFactory: (config: ConfigService) => ({
// .env 파일에 JWT_SECRET_KEY라는 키로 비밀키를 저장해두고 사용
secret: config.get<string>('JWT_SECRET_KEY'),
}),
inject: [ConfigService],
}),
],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
...
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
private jwtService: JwtService, // JWT 토큰 생성을 위해 주입한 서비스
) {}
// 로그인
async login(loginUserDto: LoginUserDto) {
const { userId, password } = loginUserDto;
const user = await this.userRepository.findOne({
where: { userId, deletedAt: null },
select: ['id', 'password'],
});
if (_.isNil(user)) {
throw new NotFoundException(`유저를 찾을 수 없습니다. ID: ${userId}`);
}
if (user.password !== password) {
throw new UnauthorizedException(
`유저의 비밀번호가 올바르지 않습니다. ID: ${userId}`,
);
}
// 추가된 코드 - JWT 토큰 생성
const payload = { id: user.id };
const accessToken = await this.jwtService.signAsync(payload);
return accessToken;
}
// 회원가입
async create(createUserDto: CreateUserDto) {
const existUser = await this.findOne(createUserDto.userId);
if (!_.isNil(existUser)) {
throw new ConflictException(
`이미 가입된 ID입니다. ID: ${createUserDto.userId}`,
);
}
const newUser = await this.userRepository.save(createUserDto);
// 추가된 코드 - JWT 토큰 생성
const payload = { id: newUser.id };
const accessToken = await this.jwtService.signAsync(payload);
return accessToken;
}
...
}
전역적으로 사용하기 위한 JWT 검증 미들웨어를 구현
코드 자체는 기존에 Express에서 사용한 Access Token 인증 미들웨어와 아주 유사함
app.module.ts 코드 구현
JWT 검증 미들웨어를 전역적으로 사용하기 위해서 app.module에서 imports
import Joi from 'joi';
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Post } from './post/entities/post.entity';
import { PostModule } from './post/post.module';
import { UserModule } from './user/user.module';
import { User } from './user/entities/user.entity';
import { JwtModule } from '@nestjs/jwt';
import { AuthMiddleware } from './auth/auth.middleware';
import { CacheModule } from '@nestjs/cache-manager';
const typeOrmModuleOptions = {
// useFactory는 동적 모듈의 속성을 설정하기 위해 사용
// useFactory에서 ConfigService를 주입받아 환경변수(.env)로부터
// 데이터베이스 설정값을 가져와서 TypeOrmModuleOptions 객체를 반환함
useFactory: async (
configService: ConfigService,
): Promise<TypeOrmModuleOptions> => ({
type: 'mysql',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [Post, User],
synchronize: configService.get('DB_SYNC'),
logging: true,
}),
// useFactory에서 사용할 의존성을 주입받기 위해 사용
inject: [ConfigService],
};
@Module({
imports: [
// forRoot는 ConfigModule의 정적인(하드코딩된) 기초 설정을 위해 사용
// 여기서는 Joi를 통한 유효성 검사 설정
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_NAME: Joi.string().required(),
DB_SYNC: Joi.boolean().required(),
}),
}),
// forRootAsync는 TypeOrmModule의 동적인 기초 설정을 위해 사용 (환경변수나 데이터베이스)
TypeOrmModule.forRootAsync(typeOrmModuleOptions),
// JwtModule이라는 동적 모듈을 설정하고 다른 user 모듈에서 사용하기 위한 코드
JwtModule.registerAsync({
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET_KEY'), // .env 파일에 JWT_SECRET_KEY라는 키로 비밀키를 저장해두고 사용
}),
inject: [ConfigService],
}),
PostModule,
UserModule,
],
controllers: [AppController],
// 모듈 내부에서 사용하기 위해서 인증 미들웨어를 providers에 추가
providers: [AppService, AuthMiddleware],
})
// 미들웨어를 사용하는 모듈은 NestModule 인터페이스를 구현해야 함
export class AppModule implements NestModule {
// 미들웨어를 구성하기 위한 configure 메서드
configure(consumer: MiddlewareConsumer) {
// 미들웨어 구성을 위한 consumer 객체
consumer
// 적용할 미들웨어를 선택
.apply(AuthMiddleware)
// user/check 경로를 GET 메서드 요청에 대해 AuthMiddleware를 적용시킴
.forRoutes({ path: 'user/check', method: RequestMethod.GET });
}
}
import { Body, Controller, Get, Post, Req } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
import { UserService } from './user.service';
// /user 주소를 사용
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
// /user/login 주소로 요청이 들어왔을 때 userService의 login 메서드 실행
@Post('/login')
async login(@Body() loginUserDTO: LoginUserDto) {
return await this.userService.login(loginUserDTO);
}
// /user/signup 주소로 요청이 들어왔을 때 userService의 create 메서드 실행
@Post('/signup')
async createUser(@Body() createUserDTO: CreateUserDto) {
return await this.userService.create(createUserDTO);
}
// /user/check 주소로 요청이 들어왔을 때 userService의 checkUser 메서드 실행
@Get('/check')
checkUser(@Req() req: any) {
const userPayload = req.user;
return this.userService.checkUser(userPayload);
}
}
인가는 인증된 사용자가 특정 작업을 수행할 권리가 있는지 확인하는 절차를 의미
예를 들면, 게시물 작성 및 수정, 삭제는 로그인된 사용자만 사용가능하기에 정상적으로 인증된 사용자인지 확인
일반적인 데코레이터는 클래스나 함수와 같은 곳에 메타 데이터를 추가하는 방법을 제공하는 것
즉, 코드에 추가적인 정보를 제공해서 실행 시점에 코드의 동작 방법이 제공됨
커스텀 데코레이터는 우리가 원하는 대로 데코레이터의 동작을 구현해서 사용하는 것을 의미함
게시판 프로젝트를 예로 들면 커스텀 데코레이터를 통해서 사용자의 정보를 추출하거나 사용자의 역할에 따라 접근을 제어할 수도 있음
커스텀 데코레이터 코드 예시
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const UserInfo = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
if (request.user) {
return request.user;
}
return null;
}
);
data
: 데코레이터의 인자로 전달될 수 있는 데이터
ctx
: Nest.js의 실행 컨텍스트를 나타내며 현재 HTTP 요청과 관련된 모든 정보를 가지고 있음 (즉, HTTP 요청의 req.user에 접근할 수 있음)
ctx.switchToHttp().getRequest()
라는 메소드를 통해서 req에 접근을 하는 것
ctx.switchToHttp().getResponse()
라는 메소드를 통해서 res에 접근을 하는 것
커스텀 데코레이터 적용 예시
import { Controller, Get } from '@nestjs/common';
import { CurrentUser } from './current-user.decorator';
@Controller('user')
export class UserController {
@Get()
getProfile(@UserInfo() user: any) {
return user;
}
}
Guard는 Nest.js에서 인가를 구현할 때 특정 라우트에 대한 접근 제어를 함
인가를 구현할 때 커스텀 데코레이터와 마찬가지로 꼭 필요한 요소
여러 모듈에서 공통적으로 수행되는 로직을 중앙에서 관리하도록 하는 프로그래밍 기법
이를 통해 코드의 중복을 줄이고 유지보수가 용이하게 구현이 가능함
그래서 로깅이나 인증, 에러 처리와 같은 기능들을 AOP 방법으로 구현함
자주 변하지 않는 데이터가 지속적으로 요청되는 경우에 매번 서버에 접근하지 않고 사용자 메모리에 보관했다가 다시 요청이 오면 메모리에서 그 데이터를 꺼내주는 방법
cache-manager 설치 명령어
npm i @nestjs/cache-manager cache-manager
...
@Module({
imports: [
// forRoot는 ConfigModule의 정적인(하드코딩된) 기초 설정을 위해 사용
// 여기서는 Joi를 통한 유효성 검사 설정
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_NAME: Joi.string().required(),
DB_SYNC: Joi.boolean().required(),
}),
}),
// forRootAsync는 TypeOrmModule의 동적인 기초 설정을 위해 사용 (환경변수나 데이터베이스)
TypeOrmModule.forRootAsync(typeOrmModuleOptions),
// JwtModule이라는 동적 모듈을 설정하고 다른 user 모듈에서 사용하기 위한 코드
JwtModule.registerAsync({
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET_KEY'), // .env 파일에 JWT_SECRET_KEY라는 키로 비밀키를 저장해두고 사용
}),
inject: [ConfigService],
}),
// 현재 모듈에서 사용할 캐시모듈을 imports
CacheModule.register({
ttl: 60000, // 데이터 캐싱 시간(밀리 초 단위, 1000 = 1초)
max: 100, // 최대 캐싱 개수
isGlobal: true,
}),
PostModule,
UserModule,
],
controllers: [AppController],
providers: [AppService, AuthMiddleware],
})
...
...
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
@Injectable()
export class PostService {
constructor(
// @InjectRepository는 어떤 엔티티(테이블)을 주입해서 사용할지 정의하는 데코레이터
@InjectRepository(Post) private postRepository: Repository<Post>,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
async create(createPostDto: CreatePostDto) {
return (await this.postRepository.save(createPostDto)).id;
}
async findAll() {
const cachedArticles = await this.cacheManager.get('articles');
// 캐싱이 되어 있으면 캐싱에 있는 articles 데이터 반환
if (!_.isNil(cachedArticles)) {
return cachedArticles;
}
const articles = await this.postRepository.find({
where: { deletedAt: null },
select: ['id', 'title', 'updatedAt'],
});
// 캐싱된 데이터가 없으면 캐시에 articles 데이터 추가
await this.cacheManager.set('articles', articles);
return articles;
}
...
}
Nest.js 프로젝트 생성, 깃허브 연결 등과 같은 기본적인 프로젝트 세팅을 진행할 예정
그리고 프로젝트에 대한 ERD와 API 명세서도 구상해서 작성할 예정
기본적인 설계 뿐만 아니라 세팅한 것들이 잘 돌아가는지 웹 서버를 구동할 예정
가능하다면 추가적인 몇가지 API도 구현할 예정
일단 주말 안으로 진행할 예정
5주차 강의를 통해서 인증, 인가에 대한 보안적인 측면에 대해 학습함
여전히 Nest.js에서 지원해주는 기능들이 많지만 활용하기는 어려운 것 같음
특히 관심이 있는 내용은 캐싱에 대한 내용임
캐싱을 통해서 성능 향상을 도모할 수 있고 Redis와 같은 데이터베이스를 사용할 수 있으니 조금 더 공부하면 재밌는 구현을 할 수 있을 것 같음
하지만 그와 같은 구현을 하기 위해서는 Nest.js와 조금 더 많이 친해질 필요가 있음