본문 바로가기

framework/NestJs

[NestJs] JWT 사용

JWT 설명

JWT란?

JSON Web Token : json 형식으로 사용하는 웹 토큰이다.

JWT 구성

1. Header 

  • base64 인코딩 토큰의 타입과 알고리즘

2. Payload 

  • base64 인코딩 데이터(key-value)
  • 필요한 정보만 넣는게 중요하다.

3. Signature

  • Header와 Payload를 조합하고 비밀키로 서명한 후 base64로 인코딩

4. JWT 예시

  • aaa.bbb.ccc

프로세스 설명

로그인(발급) 프로세스

1. 프론트엔드에서 로그인을 위한 정보를 서버로 전송한다.

2. 백엔드에서 로그인한 정보를 수신한다.

3. 백엔드에서 시크릿 키를 사용(서명)하여 JWT를 생성한다.

4. 생성한 JWT를 포론트엔드로 발급(전송)한다.

5. 프론트엔드에서 발급된 JWT를 안전한 장소에 보관한다.

안전한 공간은 로컬스토리지, http 온리 쿠키에 넣는다.

 

검증 프로세스

1. 프론트엔드에서 헤더에 발급된 JWT를 포함하여 백엔드로 요청한다.

2. JWT Guard에서 수신한다.

3. JWT Strategy를 실행하는데, 요청 헤더의 JWT를 시크릿키로 디코딩하여 사용자 정보를 얻는다.

4. 사용자 정보를 통해 비지니스 로직을 수행한다.


구현

1. passport

커뮤니티에서 잘 알려진 가장 인기 있는 node.js 인증 라이브러리이며 많은 프로덕션 애플리케이션에서 성공적으로 사용됩니다. 

$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-loca

 

2. 발급

//* AuthService class 생성
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { CatsRepository } from '../cats/cats.repository';
import { LoginRequestDto } from './dto/login.request.dto';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    // DB 연결을 위한 repository
    private readonly catsRepository: CatsRepository,
    // 발급(서명)을 위해 nestjs/jwt에서 제공하는 JwtService를 사용한다.
    private jwtService: JwtService,
  ) {}

  async jwtLogin(data: LoginRequestDto) {
    const { email, password } = data;

    //* email로 DB에서 사용자가 존재하는지 검색한다.
    const cat = await this.catsRepository.findCatByEmail(email);
    if (!cat) {
      throw new UnauthorizedException('이메일과 비밀번호를 확인해주세요.');
    }

    //* bcrypt를 통해 저장된 패스워드와 전달받은 파라미터를 비교한다.
    const isPasswordvalidated: boolean = await bcrypt.compare(
      password,
      cat.password,
    );

    if (!isPasswordvalidated) {
      throw new UnauthorizedException('이메일과 비밀번호를 확인해주세요.');
    }

    const payload = { email: email, sub: cat.id };

    return {
      // jwt를 발급한다.
      token: this.jwtService.sign(payload),
    };
  }
}
// * AuthModule

import { forwardRef, Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt/jwt.strategy';
import { CatsModule } from '../cats/cats.module';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt', session: false }),
    // Jwt발급 시 옵션을 설정한다.
    JwtModule.register({
      // 발급시 시크릿키
      secret: 'secret',
      signOptions: {
        // 만료 기간 1년
        expiresIn: '1y',
      },
    }),
    // 순환참조
    forwardRef(() => CatsModule),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

catsController에서 로그인시에 AuthService의 jwtLogin 메서드를 사용하기 위해 exports에 서비스를 작성한다.

 

// * CatsModule

import { forwardRef, Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { MongooseModule } from '@nestjs/mongoose';
import { Cat, CatSchema } from './cats.schema';
import { CatsRepository } from './cats.repository';
import { AuthModule } from '../auth/auth.module';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }]),
    forwardRef(() => AuthModule),
  ],
  controllers: [CatsController],
  providers: [CatsService, CatsRepository],
  exports: [CatsService, CatsRepository],
})
export class CatsModule {}

AuthService에서 CatsRepository를 통해 DB에 접근하므로 CatsRepository를 작성한다.

 

import부분에 CatsModule은 AuthModule을, AuthModule은 CatsModule을 import하는 순환참조가 발생한다.

이 부분을 해결하기 위해 forwartRef를 사용한다.

 

3. 검증

import { AuthGuard } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

@Injectable을 통해 의존성 주입이 가능하도록 설정한다.

AuthGuard는 stragety를 자동으로 실행해주는 기능이 존재한다.

 

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Payload } from './jwt.payload';
import { CatsRepository } from '../../cats/cats.repository';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly catsRepository: CatsRepository) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'secret',
      ignoreExpiration: false,
    });
  }

  async validate(payload: Payload) {
    const cat = await this.catsRepository.findCatByIdWithoutPassword(
      payload.sub,
    );

    if (cat) {
      return cat; // request.user 안에 cat이 있음.
    } else {
      throw new UnauthorizedException('접근 오류');
    }
  }
}

jwtFromRequest: JWT를 헤더에서 가져온다.

ignoreExpiration: 만료기간이 없는 jwt을 생성한다.

AuthGuard는 strategy를 자동으로 실행해준다고 했었는데 validate가 실행된다.

 

커스텀 데코레이션을 만들어 request.user를 더 간단히 할 수 있다.

@커스텀데코레이터명을 controller의 메소드 파라미터에 넣으면 된다.

// * 커스텀 데코레이터

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);
  @Get()
  @ApiOperation({ summary: '' })
  @UseGuards(JwtAuthGuard) // jwtGuard를 사용하는 앤드포인트
  // 커스텀 데코레이터를 통해 request.user가 아닌 데코레이터의 결과값을 cat에 담아준다.
  getCurrentCat(@CurrentUser() cat) { 
    return cat.readOnlyData;
  }

검증이 필요한 앤드포인트로 요청할 경우 header에 담아주면 된다.

또한 JwtStrategy를 생성할 때 헤더의 bearer에서 jwt를 가져온다고 했으므로 bearer를 붙어야한다.

'framework > NestJs' 카테고리의 다른 글

[NestJs] 디자인 패턴 적용  (0) 2023.03.30
[NestJs] swagger 예시  (0) 2023.03.30
[NestJs] Mongoose - 스키마, Insert  (0) 2023.03.29
[NestJs] npm 명령어  (0) 2023.03.29