인증·인가와 서버 실행
JWT 인증과 역할 기반 인가를 구현하고 API 서버의 실행 결과를 검증합니다.
인증 및 인가 구현
그림은 로그인 때의 비밀번호 검증과 이후 요청의 토큰·정책 검증을 함께 정리한 설계입니다. 매 JWT 요청에서 비밀번호 해시 비교를 다시 수행한다는 뜻은 아닙니다. 원문은 isAdmin claim을 검사하며 현재 DB 권한 재조회나 리소스별 소유권 검사는 구현하지 않습니다.
AUTHN · AUTHZ BOUNDARY
신원 확인과 현재 정책 검사는 서로 다른 경계다
자격 증명과 token claim을 검증해 좁은 principal을 만들고 매 요청마다 resource policy를 확인한 뒤 공개 DTO만 반환한다.
- Credentials
secret input
- Hash verification
constant-time compare
- Token claims
signature · exp · iss
- Principal
minimal identity
- Policy check
resource · action
- Redacted DTO
no secrets
- 핵심 경계
- 검증된 흐름
- 조건·되먹임
실제 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 };
}
}JWT_SECRET은 예제 문자열이 아닌 충분한 무작위 비밀값을 환경에서 주입하고 시작 시 누락을 거절해야 합니다. 현재 전략은 서명과 만료 검사를 설정하지만 그림의 issuer(iss)·audience 검사는 설정하지 않았습니다.
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 서버 실행 전 연결 확인
이 절은 인증 구성의 일부를 보여 주며 단독 완성 프로젝트가 아닙니다. 설치 목록에는 코드가 사용하는 @nestjs/jwt·passport-local과 bcrypt가 빠져 있고 bcryptjs가 설치됩니다. 사용할 해시 라이브러리를 일치시키고, 앞 절의 findOneByEmail·비밀번호 해시 저장을 제공하는 UserService에 LocalStrategy와 AuthModule 등록을 연결해야 합니다.
validateUser에서 비밀번호를 뺀 객체에 as User를 붙여도 누락 필드가 복구되지는 않습니다. 회원가입과 조회가 엔터티를 그대로 반환하는 경로에는 공개 DTO 변환이 필요하며, 아래 실행 명령만으로 그림의 비밀 필드 제거가 보장되지 않습니다.
API 서버 실행
모든 설정과 구현이 완료되었다면, 백엔드 서버를 실행하여 API가 정상적으로 동작하는지 확인할 수 있습니다.
packages/server 디렉토리에서 다음 명령어를 실행합니다.
npm run start:dev서버가 성공적으로 실행되면, http://localhost:4100 (또는 설정한 포트)에서 API가 서비스되고 있음을 확인할 수 있습니다.
Postman, Insomnia 또는 브라우저의 개발자 도구를 사용하여 구현된 API 엔드포인트를 테스트해볼 수 있습니다.
백엔드 구현 요약
이 절에서는 NestJS를 활용하여 타입스크립트 기반의 API 서버를 구현하는 핵심 과정을 살펴보았습니다.
데이터베이스 연동(TypeORM), DTO 유효성 검사, 환경 변수 관리, 그리고 필수적인 인증 및 인가 시스템 구현까지 다룸으로써, 실제 프로젝트에서 필요한 백엔드 기능의 상당 부분을 구축하는 방법을 익혔습니다.
NestJS의 모듈 구조와 데코레이터 문법은 타입스크립트와 함께 구조화된 API 서버를 만드는 데 도움이 됩니다.
다음 절에서는 이 API 서버와 연동될 클라이언트 애플리케이션을 구현합니다.