본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Backend

NestJS JWT 인증과 Passport

작성 2026. 10. 13.추가
목차7개 항목

JWT의 구조(Header·Payload·Signature, sub·exp·iat 클레임)와 세션 방식과의 비교는 세션 기반 인증과 토큰 기반 인증에 있다. 이 노트는 NestJS에서 Passport Strategy와 Guard로 토큰을 발급·검증하는 구현만 다룬다.

NestJS 공식 문서는 JwtService와 직접 만든 Guard로 구현하는 방법과 Passport 전략으로 구현하는 방법을 모두 안내한다. 소셜 로그인처럼 전략을 여러 개 붙일 때 Passport가 편하다.

로그인과 인증 흐름

  1. 프론트엔드가 email·password로 로그인 API를 호출한다.
  2. 백엔드는 사용자를 확인하고 비밀번호를 검증한 뒤, { email, sub: userId } payload를 비밀키로 서명해 JWT를 발급한다.
  3. 프론트엔드는 토큰을 보관한다. 보관 위치별 위험은 토큰 보관 위치에 있다.
  4. 이후 요청마다 Authorization: Bearer <token> 헤더에 토큰을 실어 보낸다.
  5. 백엔드의 JwtAuthGuard가 JwtStrategy를 실행해 서명·만료를 검증하고, payload의 sub로 사용자를 찾아 request.user에 넣는다.
  6. 핸들러는 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 auth

AuthModule

@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로 서버 데이터 가져오기(로그인 상태 판단), 세션 기반 인증과 토큰 기반 인증

참고: NestJS Authentication, Guards

댓글

GitHub 계정으로 댓글을 남길 수 있어요.