코딩캠프/내일배움캠프

[ TIL ] 02.22(수) 71일차

고랑E 2023. 2. 22. 21:00
728x90

Nest.js 심화

 

 

2. JWT 발급하기

 

 

Nest.js에서의 jwt 패키지는 @nestjs/jwt

@nestjs/jwt 설치

npm i @nestjs/jwt

 

Auth 에 대해 알아보기

  • Authentication (인증)
    • 인증은 login 함수와 같은 함수를 통해 사용자 ID와 암호와 같은 데이터를 요구하여 사용자의 신원을 파악하는 프로세스에요. 이 인증 절차를 통과한 유저만이 해당 유저임을 증빙할 수 있는 JWT 토큰을 받아요.
  • Authorization (승인 또는 인가)
    • 승인은 해당 사용자가 특정 함수 혹은 리소스에 접근할 수 있는지를 파악하는 프로세스에요. 발급받은 JWT 토큰을 토대로 서버는 특정 사용자임을 알아내고 특정 사용자에 관련된 액션은 전부 허가를 해줘요.

 

 

user.module.ts 1차

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from 'src/board/article.entity';
import { JwtConfigService } from 'src/config/jwt.config.service';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]), // 이건 TypeORM 강의 시간에 배웠죠?
    JwtModule.register({
      secret: 'secret', // 일단은 시크릿키를 하드코딩한 상태
      signOptions: { expiresIn: '3600s' }, // 토큰의 만료시간은 1시간
    }),
  ],
  providers: [UserService],
  exports: [UserService],
  controllers: [UserController],
})
export class UserModule {}

위 코드는 UserService에서 JWT 패키지를 사용할 수 있게 JwtModule.register 함수를 통해서 주입

또한 위에 시크릿키를 하드코딩할 경우 해커에게 시크릿키가 유출될 경우 계정 해킹이 될수있다.

 

 

저번에 배운것 처럼 @nest.js/config 패키지를 통해 비밀키를 캡슐화 한다.

 

config/jwt.config.service.ts 생성

import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtModuleOptions, JwtOptionsFactory } from "@nestjs/jwt";

@Injectable()
export class JwtConfigService implements JwtOptionsFactory {
  constructor(private readonly configService: ConfigService) {}

  createJwtOptions(): JwtModuleOptions {
    return {
      secret: this.configService.get<string>("JWT_SECRET"),
      signOptions: { expiresIn: "3600s" },
    };
  }
}

 

user.module.ts 2차

JwtModule.registerAsync({
      imports: [ConfigModule],
      useClass: JwtConfigService,
      inject: [ConfigService],
    }),

 

로그인 함수에서 JWT 발급받기

user.service.ts

import {
  ConflictException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import _ from 'lodash';
import { Repository } from 'typeorm';
import { User } from './user.entity';

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

  async login(userId: string, password: string) {
    const user = await this.userRepository.findOne({
      where: { userId, deletedAt: null },
      select: ['id', 'password'],
    });

    if (_.isNil(user)) {
      throw new NotFoundException(`User not found. userId: ${userId}`);
    }

    if (user.password !== password) {
      throw new UnauthorizedException(
        `User password is not correct. userId: ${userId}`,
      );
    }

    const payload = { id: user.id };
    const accessToken = await this.jwtService.signAsync(payload);
    return accessToken;
  }

  async createUser(userId: string, name: string, password: string) {
    const existUser = await this.getUserInfo(userId);
    if (!_.isNil(existUser)) {
      throw new ConflictException(`User already exists. userId: ${userId}`);
    }

    const insertResult = await this.userRepository.insert({
      userId,
      name,
      password,
    });

    const payload = { id: insertResult.identifiers[0].id };
    const accessToken = await this.jwtService.signAsync(payload);
    return accessToken;
  }

  updateUser(userId: string, name: string, password: string) {
    this.userRepository.update({ userId }, { name, password });
  }

  async getUserInfo(userId: string) {
    return await this.userRepository.findOne({
      where: { userId, deletedAt: null },
      select: ['name'], // 이외에도 다른 정보들이 필요하면 리턴해주면 됩니다.
    });
  }
}

일단, createUser가 async 함수로 바뀌었습니다. 저는 회원가입을 완료하면 곧바로 로그인 처리가 되길 원하기 때문에 바로 JWT를 발급하게끔 바꾸었어요! 그러기 위해서는 insert가 되었을 때 유저에 해당되는 id를 알아야 하기 때문에 async 함수로 바뀐것이에요!

 

또한, DI를 통해 주입된 this.jwtService의 signAsync 함수로 JWT를 발급을 하는 코드를 넣었습니다. 이제 회원가입 및 로그인에 성공하면 클라이언트는 JWT를 받을 수 있어요!

 

 

3. JWT 검증하기

 

JWT를 검증하는 Auth 미들웨어를 만들자

auth/auth.middleware.ts

import {
  Injectable,
  NestMiddleware,
  UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private jwtService: JwtService) {}

  async use(req: any, res: any, next: Function) {
    const authHeader = req.headers.authorization;

    if (!authHeader) {
      throw new UnauthorizedException("JWT not found");
    }

    let token: string;
    try {
      token = authHeader.split(" ")[1];
      const payload = await this.jwtService.verify(token);
      req.user = payload;
      next();
    } catch (err) {
      throw new UnauthorizedException(`Invalid JWT: ${token}`);
    }
  }
}

클라이언트가 헤더에 Authorization 필드로 Bearer {JWT} 를 보내면 ({JWT}에는 서버에서 실제로 받은 JWT를 채워넣어야 합니다) AuthMiddleware는 JWT를 파싱하여 특정 유저임을 파악할 수 있습니다.

 

 

AuthMiddleware는 모듈의 형태가 아니고 독립적인 서비스이기 때문에

JwtService를 DI하기 위해서 AppModule에서 JwtService를 주입해야 한다.

AppModule에도 JwtModule를 import 시켜야 한다.

 

 

app.module.ts 수정

JwtModule.registerAsync({ // AuthMilddleware에서도 사용할 수 있게 import
      imports: [ConfigModule],
      useClass: JwtConfigService,
      inject: [ConfigService],
    }),

 

 

User 컨트롤러 추가

nest g co user

 

user.controller.ts

import { Controller, Get, Post, Put } from "@nestjs/common";
import { UserService } from "./user.service";

@Controller("user")
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post("/login")
  async login() {
    return await this.userService.login("userId", "password");
  }

  @Post("/signup")
  async createUser() {
    return await this.userService.createUser("userId", "name", "password");
  }

  @Put("/update")
  updateUser() {
    this.userService.updateUser("userId", "new_name", "new_password");
  }
}

 

 

올바른 JWT를 갖고있는 사용자만이 호출할 수 있도록 설정해야 합니다.

 

app.module.ts 2차

import {
  MiddlewareConsumer,
  Module,
  NestModule,
  RequestMethod,
} from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BoardModule } from './board/board.module';
import { TypeOrmConfigService } from './config/typeorm.config.service';
import { UserModule } from './user/user.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { JwtConfigService } from './config/jwt.config.service';
import { AuthMiddleware } from './auth/auth.middleware';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useClass: TypeOrmConfigService,
      inject: [ConfigService],
    }),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useClass: JwtConfigService,
      inject: [ConfigService],
    }),
    BoardModule,
    UserModule,
  ],
  controllers: [AppController],
  providers: [AppService, AuthMiddleware], // AuthMiddleware 추가
})
export class AppModule implements NestModule {
  // NestModule 인터페이스 구현
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware) // 미들웨어 적용!
      .forRoutes({ path: 'user/update', method: RequestMethod.PUT });
  }
}

 

providers 에 AuthMiddleware를 추가하고

NestModule 인터페이스를 구현해야한다.

 

위의 코드에서는 PUT /user/update에 해당되는 API에 AuthMiddleware를 적용하겠다

유저 정보를 업데이트를 할 때 올바른 JWT를 넘겨야 유저 정보를 업데이트 할 수 있다.

 

4. JWT 기능 테스트

 

테스트를 하기 위해  user.controller.ts

import { Controller, Get, Post, Put } from "@nestjs/common";
import { UserService } from "./user.service";

@Controller("user")
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post("/login")
  async login() {
    return await this.userService.login("test_id", "test_pw");
  }

  @Post("/signup")
  async createUser() {
    return await this.userService.createUser("test_id", "test_name", "test_pw");
  }

  @Put("/update")
  updateUser() {
    this.userService.updateUser("test_id", "test_new_n", "test_new_p");
  }
}

 

테스트 해볼려고 하니까 수정해야 될 부분이 더 있었다..

 

.env 

JWT_SECRET="SECRET"

추가

 

typeorm.config.service.ts 의 엔티티에 User 추가

import { User } from 'src/user/user.entity';

      entities: [Article, User],