NestJS에서의 인증 토큰 시스템
해싱(Hashing)과 기본 토큰 개념
해싱의 중요성
인증 시스템의 기본은 안전한 비밀번호 저장입니다. 해싱은 원본 데이터를 고정 길이의 문자열로 변환하는 일방향 함수로, 원본으로 되돌릴 수 없습니다. NestJS에서는 bcrypt와 같은 라이브러리를 사용하여 비밀번호를 해싱합니다.
import * as bcrypt from 'bcrypt';
// 비밀번호 해싱
const hashPassword = async (password: string): Promise<string> => {
const salt = await bcrypt.genSalt(10);
return bcrypt.hash(password, salt);
};
// 비밀번호 검증
const validatePassword = async (
plainPassword: string,
hashedPassword: string
): Promise<boolean> => {
return bcrypt.compare(plainPassword, hashedPassword);
};토큰 기본 개념
토큰은 인증된 사용자를 식별하는 암호화된 문자열입니다. 서버는 인증 성공 시 토큰을 발급하고, 클라이언트는 이후 요청에 이 토큰을 포함시켜 자신을 인증합니다.
Passport와 인증 전략
NestJS는 Passport 라이브러리를 통해 다양한 인증 전략을 지원합니다.
Local Strategy (로컬 전략)
사용자 이름과 비밀번호를 사용한 전통적인 인증 방식입니다.
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}JWT Strategy (JWT 전략)
토큰 기반 인증을 위한 전략으로, 클라이언트가 제공한 JWT 토큰을 검증합니다.
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}JWT (JSON Web Token)
JWT는 당사자 간에 정보를 안전하게 전송하기 위한 개방형 표준입니다.
JWT 구조
JWT는 헤더(Header), 페이로드(Payload), 서명(Signature)의 세 부분으로 구성됩니다:
- 헤더: 토큰 유형과 사용된 알고리즘을 지정
- 페이로드: 클레임(사용자 ID, 역할 등)을 포함
- 서명: 토큰의 무결성을 검증
NestJS에서 JWT 구현
import { JwtService } from '@nestjs/jwt';
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && await bcrypt.compare(pass, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}모듈 설정
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60m' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}토큰 관리
토큰 발급
사용자 인증 후 JWT 토큰을 발급합니다:
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}토큰 검증
JWT 가드를 사용하여 보호된 라우트에 대한 액세스를 제어합니다:
@Controller('profile')
export class ProfileController {
@UseGuards(JwtAuthGuard)
@Get()
getProfile(@Request() req) {
return req.user;
}
}토큰 갱신
토큰 만료 시 갱신 메커니즘을 구현합니다:
@Injectable()
export class AuthService {
// ... 기존 코드
async refreshToken(refreshToken: string) {
try {
// 리프레시 토큰 검증
const payload = this.jwtService.verify(refreshToken, {
secret: jwtConstants.refreshSecret,
});
// 새 액세스 토큰 발급
const newAccessToken = this.jwtService.sign(
{ username: payload.username, sub: payload.sub },
{ expiresIn: '1h' }
);
return {
access_token: newAccessToken,
};
} catch (e) {
throw new UnauthorizedException('Invalid refresh token');
}
}
}토큰 만료 및 블랙리스트
보안 강화를 위해 로그아웃 시 토큰을 블랙리스트에 추가할 수 있습니다:
@Injectable()
export class TokenBlacklistService {
private readonly blacklist: Set<string> = new Set();
addToBlacklist(token: string): void {
this.blacklist.add(token);
}
isBlacklisted(token: string): boolean {
return this.blacklist.has(token);
}
}보안 고려사항
- 환경 변수 사용: JWT 비밀 키는 환경 변수로 관리
- 토큰 만료 시간: 짧은 만료 시간 설정 (액세스 토큰 1시간, 리프레시 토큰 2주)
- HTTPS: 모든 API 통신은 HTTPS로 암호화
- XSS 및 CSRF 방어: HttpOnly 쿠키 사용 고려
- 토큰 저장소: 클라이언트 측에서는 안전한 저장소(HttpOnly 쿠키, 로컬 스토리지 대신)에 저장
이러한 인증 및 토큰 관리 시스템은 안전하고 확장 가능한 NestJS 애플리케이션 구축의 기반이 됩니다.
더 상세한 정보나 특정 부분에 대해 더 알고 싶으시다면 말씀해 주세요!