인증·인가와 서버 실행
JWT 인증과 역할 기반 인가를 구현하고 API 서버의 실행 결과를 검증합니다.
인증 및 인가 구현
실제 API 서버에서는 사용자의 신원을 확인하는 인증(Authentication)과, 특정 리소스 접근 권한을 확인하는 인가(Authorization) 과정이 필수적입니다.
NestJS에서는 @nestjs/passport와 passport.js를 통합해 이를 비교적 쉽게 구현할 수 있습니다.
JWT(JSON Web Token)는 RESTful API에서 가장 흔히 사용되는 인증 방식 중 하나입니다.
npm install @nestjs/passport passport passport-jwt @types/passport-jwt @types/passport bcryptjs
npm install --save-dev @types/bcryptjsAuthModule 및 AuthService 생성:
로그인, 회원가입, JWT 발급 등의 로직을 처리하는 모듈과 서비스를 만듭니다.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from '../user/entities/user.entity';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService, // JwtService 주입
) {}
async validateUser(email: string, pass: string): Promise<User | null> {
const user = await this.userService.findOneByEmail(email);
if (user && (await bcrypt.compare(pass, user.password))) {
// 비밀번호는 제외하고 반환
const { password, ...result } = user;
return result as User; // Partial<User>를 User로 캐스팅
}
return null;
}
async login(user: User) {
const payload = { email: user.email, sub: user.id, isAdmin: user.isAdmin }; // JWT 페이로드
return {
accessToken: this.jwtService.sign(payload),
};
}
}JwtStrategy 정의:
JWT 토큰을 검증하고 사용자 정보를 추출하는 전략을 구현합니다.
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config'; // ConfigService 임포트
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearer 토큰에서 JWT 추출
ignoreExpiration: false, // 만료된 토큰 무시 안 함
secretOrKey: configService.get<string>('JWT_SECRET'), // .env에서 JWT 비밀 키 가져오기
});
}
async validate(payload: any) { // payload는 JWT에 서명된 데이터
// 이 부분에서 실제 사용자 정보를 DB에서 가져오거나, 페이로드만 반환할 수 있습니다.
// req.user 에 페이로드가 저장됩니다.
return { userId: payload.sub, email: payload.email, isAdmin: payload.isAdmin };
}
}.env 파일에 JWT_SECRET=your_jwt_secret_key 추가
AuthModule 설정:
JwtModule과 PassportModule을 구성하고 서비스, 전략을 등록합니다.
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module';
import { LocalStrategy } from './local.strategy'; // 로컬 전략 (사용자 이름/비밀번호)
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';
@Module({
imports: [
UserModule, // UserService를 사용하기 위해 UserModule 임포트
PassportModule,
JwtModule.registerAsync({ // 비동기로 JWT 설정 (ConfigService 주입)
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '60m' }, // 토큰 만료 시간
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy], // 전략 등록
exports: [AuthService], // AuthService를 다른 모듈에서 사용 가능하도록 내보내기
})
export class AuthModule {}AuthController 구현:
로그인, 회원가입 엔드포인트를 정의합니다.
import { Controller, Post, Request, UseGuards, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard'; // 로컬 인증 가드 (추후 생성)
import { CreateUserDto } from '../user/dto/create-user.dto';
import { UserService } from '../user/user.service';
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private userService: UserService, // 회원가입을 위해 UserService 사용
) {}
@Post('register')
@HttpCode(HttpStatus.CREATED)
async register(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@UseGuards(LocalAuthGuard) // LocalAuthGuard를 사용하여 로컬 전략 실행
@Post('login')
@HttpCode(HttpStatus.OK) // 200 OK 응답
async login(@Request() req: any) { // @Request() 데코레이터로 요청 객체 접근
// LocalAuthGuard가 req.user에 사용자 정보를 저장합니다.
return this.authService.login(req.user);
}
}인증 가드 (local-auth.guard.ts, jwt-auth.guard.ts):
passport-jwt와 passport-local을 NestJS 가드로 래핑합니다.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {} // 'local' 전략 사용import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {} // 'jwt' 전략 사용이제 UserController나 다른 컨트롤러에 @UseGuards(JwtAuthGuard)를 적용하여 인증된 사용자만 접근할 수 있도록 설정합니다.
역할 기반 인가 구현
특정 API 엔드포인트에 관리자만 접근 가능하도록 하는 등 역할 기반 인가는 다음과 같이 구현할 수 있습니다.
packages/server/src/auth/roles.decorator.ts)import { SetMetadata } from '@nestjs/common';
// import { UserRole } from '@my-fullstack-app/shared/interfaces'; // shared 패키지에서 역할 정의
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);UserRole은 shared 패키지에 정의된 enum 또는 type일 수 있습니다.
export enum UserRole {
User = 'user',
Admin = 'admin',
}
export interface IUser {
id: number;
email: string;
name: string;
isAdmin: boolean; // 또는 role: UserRole;
}packages/server/src/auth/roles.guard.ts)import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
// import { UserRole } from '@my-fullstack-app/shared/interfaces';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(), // 메서드 레벨 데코레이터
context.getClass(), // 클래스 레벨 데코레이터
]);
if (!requiredRoles) {
return true; // 역할 제한이 없으면 접근 허용
}
const { user } = context.switchToHttp().getRequest();
// req.user에 저장된 사용자의 역할(isAdmin 또는 role)을 확인
// 여기서는 예시로 isAdmin 필드를 사용
return requiredRoles.some((role) => {
if (role === 'admin') {
return user.isAdmin === true;
}
// 다른 역할 검증 로직 추가
return false;
});
}
}// ...
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator';
import { UserRole } from '@my-fullstack-app/shared/interfaces/user'; // shared 인터페이스 임포트
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard) // JwtAuthGuard 먼저 실행 후 RolesGuard 실행
export class UserController {
// ...
@Get()
@Roles(UserRole.Admin) // 관리자 역할만 접근 가능
async findAll() {
return this.userService.findAll();
}
// ...
}API 서버 실행
모든 설정과 구현이 완료되었다면, 백엔드 서버를 실행하여 API가 정상적으로 동작하는지 확인할 수 있습니다.
packages/server 디렉토리에서 다음 명령어를 실행합니다.
npm run start:dev서버가 성공적으로 실행되면, http://localhost:4100 (또는 설정한 포트)에서 API가 서비스되고 있음을 확인할 수 있습니다.
Postman, Insomnia 또는 브라우저의 개발자 도구를 사용하여 구현된 API 엔드포인트를 테스트해볼 수 있습니다.
백엔드 구현 요약
이 절에서는 NestJS를 활용하여 타입스크립트 기반의 API 서버를 구현하는 핵심 과정을 살펴보았습니다.
데이터베이스 연동(TypeORM), DTO 유효성 검사, 환경 변수 관리, 그리고 필수적인 인증 및 인가 시스템 구현까지 다룸으로써, 실제 프로젝트에서 필요한 백엔드 기능의 상당 부분을 구축하는 방법을 익혔습니다.
NestJS의 모듈 구조와 데코레이터 문법은 타입스크립트와 함께 구조화된 API 서버를 만드는 데 도움이 됩니다.
다음 절에서는 이 API 서버와 연동될 클라이언트 애플리케이션을 구현합니다.
다음 다이어그램은 TypeScript API 서버에서 요청 DTO, 서비스, 응답, 예외 경계를 함께 설계하는 기준입니다.
아래 다이어그램은 API 서버 구현에서 NestJS 초기 설정, 비즈니스 로직, 배포 전 확인 지점을 연결합니다.
API 서버 구현을 마무리하기 전에는 모듈 경계, DTO 검증, 서비스 책임, 예외 응답, 배포 환경값을 한 번에 점검합니다.
이 기준이 맞아야 서버 구조와 운영 조건이 같은 계약을 유지합니다.