Nest.js

[Nest.js] Passport, JWT 이용해서 토큰 인증 후 유저 정보 가져오기

Hoo_Dev 2023. 1. 30. 15:14

JWT란?

JWT (JSON Web Token)는 당사자 간에 정보를 JSON 개체로 안전하게 전송하기위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준(RFC 7519)이다. 이 정보는 디지털 서명이 되어 있으므로 확인하고 신뢰할 수 있다.

  • 간단하게 얘기하자면 정보를 안전하게 전할 때 혹은 유저의 권한 같은 것을 체크를 하기 위해서 사용하는데 유용한 모듈이다.

Passport 모듈과 함께 JWT 인증 처리해보기

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

JWT 토큰 생성받기

 

모듈에 JWT, Passport 등록해주기

// 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';
import { UserRepository } from './user.repository';

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

 

service에서 토큰 생성하기

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthCredentialDto } from './dto/auth-credential.dto';
import { User } from './user.entity';
import { UserRepository } from './user.repository';
import * as bcrypt from 'bcryptjs';
import { UnauthorizedException } from '@nestjs/common/exceptions';
import { JwtService } from '@nestjs/jwt/dist';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(UserRepository)
    private userRepository: UserRepository,
    private jwtService: JwtService,
  ) {}

  async signUp(authCredentialDto: AuthCredentialDto): Promise<void> {
    return this.userRepository.createUser(authCredentialDto);
  }

  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))) {
      // 유저 토큰 생성(secret + payload)
      const payload = { username };
      const accessToken = await this.jwtService.sign(payload);

      return { accessToken: accessToken };
    } else {
      throw new UnauthorizedException('login failed');
    }
  }
}

jwtService의 sign()을 통해 로그인 시 토큰을 생성 받을 수 있다.

 

controller 작성

import { Controller, Post, Body, 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);
  }
}

리턴 받는 객체 타입은 { accessToken : token값 } 형식으로 들어오게 되므로 타입을 맞춰 지정해준다.

 

Passport를 해서 요청 안에서 유저 정보 얻기.

유저가 요청을 보낼 때 그 요청 안에 있는 Header에 토큰을 넣어서 요청을 보내게 된다. 그것을 통해서 요청 안에 유저 정보가 들어 있게 해줄 수 있다. 그 유저 정보를 통해서 인증 처리 혹은 권한 처리 같은 것을 해줄 수 있는데, 이러한 처리를 쉽게 해주는 것이 Passport 모듈이다.

 

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 { User } from './user.entity';
import { UserRepository } from './user.repository';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(UserRepository)
    private userRepository: UserRepository,
  ) {
    super({
      // 토큰이 유효한지 확인하기 위한 키
      secretOrKey: 'Secret1234',
      // 클라이언트에서 오는 토큰이 어디에서 오는지 명시 해 줌
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    });
  }

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

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

    return user;
  }
}

 

controller에 인증 관련 테스트 코드 작성

import {
  Controller,
  Post,
  Body,
  ValidationPipe,
  Req,
  UseGuards,
} 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('/test')
  @UseGuards(AuthGuard())
  test(@Req() req) {
    console.log('req', req);
  }
}

위와 같이 @UseGuards(AuthGuard()) 데코레이터(허용된 유저가 아니면 요청 자체를 막는 데코레이터)를 넣어 준 후 토큰 값(로그인 요청 후 받은 토큰 값)과 함께 api 요청을 보내게 되면

유저 정보가 담긴 객체가 반환이 된다.

 

만약 존재하지 않는 토큰 값을 포함하여 요청을 보내게 된다면

{
    "statusCode": 401,
    "message": "Unauthorized"
}

이와 같은 오류 메세지를 반환 해 준다.

결론적으론 요청 후 req.user로 접근을 하게 된다면 해당 토큰 유저의 정보에 대해 접근이 가능하다.

그렇다면 req.user가 아닌 바로 user라는 파라미터로 가져올 수 있는 방법은? - 커스텀 데코레이터 사용하기

 

 

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;
  },
);

controller 수정

import {
  Controller,
  Post,
  Body,
  ValidationPipe,
  Req,
  UseGuards,
} 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('/test')
  @UseGuards(AuthGuard())
  test(@GetUser() user: User) {
    console.log('user', user);
  }
}

위와 같이 커스텀 데코레이터를 사용해서 요청을 보내게 된다면

user User {
  id: 10,
  username: 'GHGHgh',
  password: '$2a$10$JQYn2m5zYOaybzYUMw8BteEb3TWEkFYILU8sPyw2JtPhLS37jZnPm'  
}

이와 같이 user 객체만 따로 반환 시켜 가져올 수 있게 된다.