본문 바로가기

프로그램 개발

NestJS 개발 시작하기(로그인 인증 개발); 6. jwt 인증 (1)

NestJS 개발 시작하기(로그인 인증 개발)

1. NestJS와 Spring의 비교

2. NestJS CLI로 개발시작

3. API개발과 Swagger

4. TypeORM으로 DB연결

5. Session, JWT, OAuth 차이

6. jwt 인증 (1)

7. 암호화와 해쉬 함수

 

6. @nestjs/jwt

a. JWT token 생성

이제, JWT와 OAuth를 사용해서 인증하는 프로그램을 만들어 보자. 앞에서 본 OAuth인증절차에서 가장 먼저 해야 할 일은 id, password를 사용해서 로그인하면 Token을 생성하는 일이다.

 

먼저, JWT Token 패키지를  @nestjs/jwt 패키지를 설치한다.

 

pnpm install @nestjs/jwt

 

설치된 @nestjs@jwt 패키지를 사용해서 OAuth를 구현하는 곳 모듈, auth.module.ts에 JwtModule을 설정한다. secret은 외부에 노출하면 안 되고 복잡한 암호로 설정하고, ConfigModule를 통해 .env에서 가져오는 것이 좋다.

 

...
@Module({
  imports: [
    ConfigModule,
    TypeOrmModule.forFeature([Account]),
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        global: true,
        secret: configService.get('JWT_SECRET'),
        signOptions: { expiresIn: '1d' }
      }),
      inject: [ConfigService]
    })
    ],
  controllers: [SignController],
  providers: [SignService]
})
...

 

Controller의 signIn을 아래와 같이 Post방식으로 고치고 Body의 데이터 구조를 signInDto로 정의한다.

 

import { ApiProperty } from "@nestjs/swagger";

export class SignInDto {
    @ApiProperty()
    email: string;

    @ApiProperty()
    password: string;
}

 

Controllerdml  signIn은 이제 성공했을 때 아래와 같이 access_token을 결과로 돌려준다.

 

...
    @Post('in/:id')
    async signIn(@Body() signInDto: SignInDto, @Req() req, @Res() res) {
        console.debug('Sign in\n' + 'SignInDto = ' + JSON.stringify(signInDto));
        try {
            const result = await this.signService.login(signInDto);
            return res.status(HttpStatus.OK).send(JSON.stringify(result));
        } catch (e) {
            return res.status(HttpStatus.UNAUTHORIZED).send('Sign in failed');
        }
    }
...

 

Service의 login은 Controller에서 필요한 access_token을 만들기 위해 jwtService를 사용하고 jwt의 payload는 email대신 ACCOUNT테이블의 primary index key를 id로 저장해서 token을 만든다. JWT의 표준에 대해 앞에서 언급한 것처럼, payload는 id나 exp처럼 이미 예약된 키도 있지만 예약되지 않은 key값을 임의로 선택하고 정보를 전달해도 된다.

 

...
import { JwtService } from '@nestjs/jwt';
import { SignInDto } from '../dto/SignInDto';

@Injectable()
export class SignService {
    constructor(@InjectRepository(Account) private repo: Repository<Account>, private jwtService: JwtService) { }
...
    async login(signInDto: SignInDto): Promise<{}> {
        const existingAccount = await this.find(signInDto.email);
        if (existingAccount?.password !== signInDto.password) {
            throw new UnauthorizedException();
        }
        const payload = { id: existingAccount.id };
        return {
            access_token: await this.jwtService.signAsync(payload)
        }
    }
...

 

login함수를 사용해서 만든 access_key를 Base64Url로 디코딩한 후 구조를 살펴보면, Header는

 

{
  "alg": "HS256",
  "typ": "JWT"
}

 

payload는 아래와 같이 Service에서 등록한 id와 iat (issued at; 토큰이 생성된 시간), exp (expired at; 토큰이 만료될 시간)이 자동으로 추가된 것을 확인할 수 있다.

 

{
  "id": "6a8c8e8b-0313-4f75-bc92-ac42a49e69ba",
  "iat": 1728774781,
  "exp": 1728861181
}

b. Guard

NestJS 개발 시작하기 시리즈의 첫 번째 글에는 NestJS에 API 요청을 하면 어떤 컴포넌트를 거쳐 응답이 되는지에 대한 흐름도가 나타나 있다.

 

NestJS 개발 시작하기(로그인 인증 개발); 1. NestJS와 Spring의 비교

NestJS 개발 시작하기(로그인 인증 개발)1. NestJS와 Spring의 비교2. NestJS CLI로 개발시작3. API개발과 Swagger4. TypeORM으로 DB연결5. Session, JWT, OAuth 차이  프로그램 개발은 고객의 요구사항, 시장의 요구사

front-it.tistory.com

Guard는 NestJS가 제공하는 AOP영역 흐름에서 가장 먼저 만나는 부분이다. Guard는 들어온 요청을 Controller에 구현된 Handler로 요청을 전달해야 할지에 대한 결정을 하며 Authentication(인증)이나 Authorization(인가) 로직은 구현할 수 있는 공간이다.

 

Guard 또한 NestJS CLI로 파일을 만들 수 있다.

 

nest generate guard auth/sign --flat
-- 또는
nest g gu auth/sign --flat

 

파일이 만들어지면 아래와 같이 Guard내용을 작성한다.

 

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';

@Injectable()
export class SignGuard implements CanActivate {
  constructor(private jwtService: JwtService) { }

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const authorization = request.headers['authorization'];
    if (!authorization) {
      throw new UnauthorizedException();
    }
    const [type, token] = authorization.split(' ');
    if (!token || type !== 'Bearer') {
      throw new UnauthorizedException();
    }
    try {
      this.jwtService.verify(token);
    } catch (e) {
      throw new UnauthorizedException();
    }
    return true;
  }
}

 

throw new UnauthorizationException() 대신 return false로 하면 인증을 받지 못하면 아래와 같은 HTTP 403 에러가 나오기 때문에 403을 사용해도 문제가 없다면 return false를 사용해도 된다.

 

{
  "message": "Forbidden resource",
  "error": "Forbidden",
  "statusCode": 403
}

 

JWT를 적용하고 Swagger를 사용하기 위해서는 main.ts의 설정과 

 

// main.ts
...
  const config = new DocumentBuilder()
    .setTitle('Auth test')
    .setDescription('auth API')
    .setVersion('1.0')
    .addTag('auth')
    .addBearerAuth()
    .build();
...

 

JWT를 적용하려는 Controller (Class전체에 적용할 수도 있고, 하나의 함수에만 적용할 수도 있다)에 ApiBearerAuth를 추가해야 한다.

 

...
    @ApiBearerAuth()
    @UseGuards(SignGuard)
    @Get('jwtapplied')
    jwtapplied(@Req() req, @Res() res) {
...

 

Swagger에 JWT인증이 적용되면 아래와 같이 Authorize버튼이 나타나고, JWT인증이 적용된 함수에는 자물쇠 표시가 나타나는데, 인증이 성공하면 자물쇠가 잠기고 해당 API를 테스트할 수 있게 된다.