목차7개 항목
JWT의 구조(Header·Payload·Signature, sub·exp·iat 클레임)와 세션 방식과의 비교는 세션 기반 인증과 토큰 기반 인증에 있다. 이 노트는 NestJS에서 Passport Strategy와 Guard로 토큰을 발급·검증하는 구현만 다룬다.
NestJS 공식 문서는 JwtService와 직접 만든 Guard로 구현하는 방법과 Passport 전략으로 구현하는 방법을 모두 안내한다. 소셜 로그인처럼 전략을 여러 개 붙일 때 Passport가 편하다.
로그인과 인증 흐름
- 프론트엔드가 email·password로 로그인 API를 호출한다.
- 백엔드는 사용자를 확인하고 비밀번호를 검증한 뒤,
{ email, sub: userId }payload를 비밀키로 서명해 JWT를 발급한다. - 프론트엔드는 토큰을 보관한다. 보관 위치별 위험은 토큰 보관 위치에 있다.
- 이후 요청마다
Authorization: Bearer <token>헤더에 토큰을 실어 보낸다. - 백엔드의 JwtAuthGuard가 JwtStrategy를 실행해 서명·만료를 검증하고, payload의
sub로 사용자를 찾아request.user에 넣는다. - 핸들러는
request.user를 이용해 비즈니스 로직을 수행한다.
Guard는 요청 생명주기에서 미들웨어 다음, 인터셉터·파이프보다 먼저 실행되어 "이 요청을 통과시킬지"를 결정한다.
구현
npm i @nestjs/passport passport @nestjs/jwt passport-jwt
npm i -D @types/passport-jwt
nest g module auth
nest g service authAuthModule
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt', session: false }),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1y' },
}),
forwardRef(() => CatsModule), // CatsRepository 사용 (순환 import → forwardRef)
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}비밀키는 반드시 환경 변수로 관리한다.
process.env로드 순서 문제를 피하려면JwtModule.registerAsync({ inject: [ConfigService], useFactory: ... })를 쓰는 것이 안전하다. 위의 만료 기간 1년은 예시일 뿐이다. JWT는 발급 뒤 서버가 강제로 무효화하기 어려우므로, 실제로는 수 분~수십 분짜리 Access Token과 교체(rotation)되는 Refresh Token을 함께 쓴다.
로그인 서비스: 토큰 발급
@Injectable()
export class AuthService {
constructor(
private readonly catsRepository: CatsRepository,
private readonly jwtService: JwtService, // JwtModule이 제공
) {}
async jwtLogIn({ email, password }: LoginRequestDto) {
const cat = await this.catsRepository.findCatByEmail(email);
// 이메일이 없든 비밀번호가 틀리든 같은 메시지 → 계정 존재 여부를 노출하지 않음
if (!cat || !(await bcrypt.compare(password, cat.password))) {
throw new UnauthorizedException('이메일과 비밀번호를 확인해주세요.');
}
const payload = { email: cat.email, sub: cat.id };
return { token: this.jwtService.sign(payload) };
}
}Strategy와 Guard: 토큰 검증
// auth/jwt/jwt.strategy.ts
export type Payload = { email: string; sub: string };
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly catsRepository: CatsRepository) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearer 토큰 추출
ignoreExpiration: false, // 만료된 토큰 거부
secretOrKey: process.env.JWT_SECRET,
});
}
// 서명 검증이 끝난 payload를 받아 사용자 조회. 반환값이 request.user가 된다
async validate(payload: Payload) {
const cat = await this.catsRepository.findCatByIdWithoutPassword(payload.sub);
if (!cat) throw new UnauthorizedException('접근 오류');
return cat;
}
}// auth/jwt/jwt.guard.ts: AuthGuard('jwt')가 위 Strategy를 자동 실행
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}Custom Decorator로 현재 사용자 꺼내기
핸들러에서 @Req() req로 req.user를 꺼내는 대신, 의도가 드러나는 파라미터 데코레이터를 만든다.
// common/decorators/user.decorator.ts
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user,
);@Post('login')
logIn(@Body() data: LoginRequestDto) {
return this.authService.jwtLogIn(data);
}
@UseGuards(JwtAuthGuard)
@Get()
getCurrentCat(@CurrentUser() cat: Cat) {
return cat.readOnlyData;
}핵심 정리
- 로그인 = 사용자 검증 후
jwtService.sign(payload)로 토큰 발급. - 인증 =
@UseGuards(JwtAuthGuard)→JwtStrategy.validate()→request.user. - 비밀키는 환경 변수로, payload에는 민감 정보 없이 식별자만 넣는다.
관련 노트: 쿠키와 세션, 웹 보안: CSRF와 XSS, SWR로 서버 데이터 가져오기(로그인 상태 판단), 세션 기반 인증과 토큰 기반 인증