Skip to content

7. JWT(JSON Web Token)


1. JWT

로그인을 할 때 로그인한 유저를 위한 토큰을 생성한 후 제공해야 한다. 이 때 JWT라는 모듈을 사용한다. JWT는 정보를 JSON 객체로 안전하게 전송하기 위한 스펙이다. 정보를 안전하게 전송할 때 혹은 유저의 권한과 같은 것을 검사하기 위해 사용하는 것이다.


JWT의 구조는 Header, Payload, Verify Signature로 이루어져 있다. Header는 타입, 해싱 알고리즘 등 토큰에 대한 메타데이터를 포함하고 있다. Payload는 유저 정보, 만료 기간, 주제 등을 포함하고 있다. Verify Signature는 토큰이 보낸 사람에 의해 서명되었으며 어떤 식으로든 변경되지 않았는지 확인하는 데 사용된다. 이 Verify Signature는 Header 및 Payload, 서명 알고리즘, 비밀키 또는 공개키를 사용하여 생성된다.


요청으로부터 온 headerspayload에, 서버에서 가지고 있는 Secret을 결합해 Verify Signature를 다시 생성한다. 처음 생성했던 JWT의 Verify Signature와 동일한지 여부를 확인한다.


JWT를 이용해서 인증을 처리하기 위해 다음과 같은 모듈을 설치한다.


npm install --save @nestjs/jwt @nestjs/passport passport-jwt passport


Auth 모듈에 PassportModuleJwtModule을 등록한다.


src/auth/auth.module.ts
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { User } from "./user.entity";

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: "jwt" }),
    JwtModule.register({
      secret: "Secret1234",
      signOptions: {
        expiresIn: 3600,
      },
    }),
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}


2. 로그인 성공 시 JWT를 이용해서 토큰 생성해 주기

Auth 모듈에 JwtModule을 등록했기 때문에 AuthService에서 가져와 사용할 수 있다.


import {
  ConflictException,
  Injectable,
  InternalServerErrorException,
  UnauthorizedException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
import { User } from "./user.entity";
import * as bcrypt from "bcryptjs";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
    private jwtService: JwtService
  ) {}

  async signUp(authCredentialDto: AuthCredentialDto): Promise<void> {
    const { username, password } = authCredentialDto;

    const salt = await bcrypt.genSalt();
    const hashedPassword = await bcrypt.hash(password, salt);

    const user = this.userRepository.create({
      username,
      password: hashedPassword,
    });

    try {
      await this.userRepository.save(user);
    } catch (error) {
      if (error.code === "23505") {
        throw new ConflictException("Existing username");
      } else {
        throw new InternalServerErrorException();
      }
    }
  }

  async signIn(
    authCredentialDto: AuthCredentialDto
  ): Promise<{ accessToken: string }> {
    const { username, password } = authCredentialDto;
    const user = await this.userRepository.findOneBy({ username });

    if (user && (await bcrypt.compare(password, user.password))) {
      const payload = { username };
      const accessToken = this.jwtService.sign(payload);

      return { accessToken };
    } else {
      throw new UnauthorizedException("login failed");
    }
  }
}
import { Body, Controller, Post, ValidationPipe } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";

@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post("/signup")
  signUp(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<void> {
    return this.authService.signUp(authCredentialDto);
  }

  @Post("/signIn")
  signIn(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<{ accessToken: string }> {
    return this.authService.signIn(authCredentialDto);
  }
}


3. Passport를 사용해서 요청 내의 유저 정보 얻기

이전 작업은 토큰을 생성해서 제공하는 목적이었고, 지금부터 할 것은 토큰을 인증하는 작업이다. PassportModule은 요청 내의 유저 정보를 이용해서 인증 처리 등의 작업을 쉽게 처리할 수 있게 한다.


먼저 다음과 같이 필요한 모듈을 설치한다.


npm install --save @types/passport-jwt


다음과 같이 JwtStrategy를 정의한다.


src/auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { InjectRepository } from "@nestjs/typeorm";
import { ExtractJwt, Strategy } from "passport-jwt";
import { Repository } from "typeorm";
import { User } from "./user.entity";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>
  ) {
    super({
      secretOrKey: "Secret1234",
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    });
  }

  async validate(payload: any) {
    const { username } = payload;
    const user: User = await this.userRepository.findOneBy({ username });

    if (!user) {
      throw new UnauthorizedException();
    }

    return user;
  }
}


이제 이 JwtStrategy를 사용하기 위해 Auth 모듈의 providers 배열에 넣어준다. 그리고 다른 모듈에서 JwtStrategyPassportModule을 사용해야 하기 때문에 exports 배열에 넣어준다.


src/auth/auth.module.ts
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./jwt.strategy";
import { User } from "./user.entity";

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: "jwt" }),
    JwtModule.register({
      secret: "Secret1234",
      signOptions: {
        expiresIn: 3600,
      },
    }),
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}


4. 요청 안에 유저 정보(User 객체)가 들어가게 하는 방법

현재 요청 내에는 유저 정보가 존재하지 않기 때문에 인증 처리를 할 수 없다. 이를 해결하기 위해서 라우트 핸들러에 @UseGuards 데코레이터와 AuthGuard()를 이용한다.


Guard, Interceptor 등 미들웨어가 불리는 순서는 다음과 같다.


Middleware → Guard → Interceptor (before) → pipe → controller → service → controller → interceptor (after) → filter → client


src/auth/auth.controller.ts
import {
  Body,
  Controller,
  Post,
  Req,
  UseGuards,
  ValidationPipe,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";

@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post("/signup")
  signUp(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<void> {
    return this.authService.signUp(authCredentialDto);
  }

  @Post("/signIn")
  signIn(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<{ accessToken: string }> {
    return this.authService.signIn(authCredentialDto);
  }

  @Post("/test")
  @UseGuards(AuthGuard())
  test(@Req() req) {
    console.log("req", req);
  }
}


test() 라우트 핸들러에 요청을 보내면 다음과 같이 user 프로퍼티의 값으로 User 객체가 확인된다.


...

user: User {
    id: 3,
    username: 'user2',
    password: '$2a$10$yW.CLGOfiCGHwHG00miri.Yc8EVlBy71AupqsKsFlifp/vMLZ3bCG'
  },

...


5. 커스텀 데코레이터 생성하기

위의 user 프로퍼티에 접근하려면 항상 req.user처럼 접근해야 한다. user라는 파라미터로 바로 가져올 수 있게 커스텀 데코레이터를 구현해 보자.


src/auth/get-user.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import { User } from "./user.entity";

export const GetUser = createParamDecorator(
  (data, ctx: ExecutionContext): User => {
    const req = ctx.switchToHttp().getRequest();
    return req.user;
  }
);


이 데코레이터를 라우트 핸들러에 달아준다.


src/auth/auth.controller.ts
import {
  Body,
  Controller,
  Post,
  UseGuards,
  ValidationPipe,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
import { GetUser } from "./get-user.decorator";
import { User } from "./user.entity";

@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post("/signup")
  signUp(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<void> {
    return this.authService.signUp(authCredentialDto);
  }

  @Post("/signIn")
  signIn(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto
  ): Promise<{ accessToken: string }> {
    return this.authService.signIn(authCredentialDto);
  }

  @Post("/test")
  @UseGuards(AuthGuard())
  test(@GetUser() user: User) {
    console.log("user", user);
  }
}

References