내배캠 99일차

·2023년 2월 20일
1

내일배움캠프

목록 보기
109/142
post-thumbnail

회원가입 / 로그인 기능 구현

User 테이블 생성

NestJS로 회원가입 / 로그인 기능을 구현하기 위해서는 그 기능의 베이스가 되는 데이터베이스 테이블(User 테이블)이 존재해야 함!

User 테이블을 User 엔티티를 만듦으로써 자연스럽게 만들어질 수 있도록 할 것!

user.entity.ts

import {
  Column,
  CreateDateColumn,
  DeleteDateColumn,
  Entity,
  Index,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from "typeorm";

@Entity({ schema: "board", name: "users" })
export class User {
  @PrimaryGeneratedColumn({ type: "int", name: "id" })
  id: number;

  @Index({ unique: true })
  @Column()
  userId: string;

  @Column("varchar", { length: 10 })
  name: string;

  @Column("varchar", { length: 10, select: false })
  password: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date | null;

  @DeleteDateColumn()
  deletedAt: Date | null;
}

User 모듈 / 서비스 추가

이제 로그인 기능을 만들고 로그인 기능을 통하여 JWT를 얻은 다음에 회원만이 할 수 있는 API에 연동을 하자!

그 전에 일단 User 모듈 / 서비스 추가를 하기 위해 다음 명령어를 입력할 것

nest g mo user
nest g s user

user.service.ts (JWT 발급 전)

회원가입 및 로그인을 성공하고 나면 JWT 발급을 해서 회원으로 로그인을 해야 사용할 수 있는 기능들을 호출할 때 일일이 로그인을 하는 방식이 아니라 최초에 로그인을 성공하면 서버에서 어떤 값을 클라이언트에게 전달하여 이 값을 서버에 주면 회원을 인식하도록 만들어 줄 것!

그 방식으로 가장 널리 쓰이는 방식이 express에서도 사용한 JWT!

import {
  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}`
      );
    }

    // JWT 발급 전
  }

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

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

		// JWT 발급 전
  }

  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"], // 이외에도 다른 정보들이 필요하면 리턴해주면 됩니다.
    });
  }
}

JWT발급

@nestjs/jwt를 설치

Nest.js에서 사용하는 JWT 패키지인 @nestjs/jwt를 설치

해당 패키지를 이용해서 편하게 JWT를 발급하고 유효성 검사를 할 수 있음.

npm i @nestjs/jwt

Auth

특정 패키지나 리포지토리를 사용하기 위해서는 모듈 데코레이터에 있는 imports 속성에 정의를 해야 함!

Auth 종류

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

여기서 Authentication을 하기 위해서는 사용자 ID에 대한 암호 일치 여부를 파악해야 함. 그런데, 이걸 하기위해서는 리포지토리를 사용해야 합니다. Auth 미들웨어가 어떠한 리포지토리도 참조하기를 원하지 않기 때문에 login 함수는 User 모듈에서, 그 이후에 회원만이 부를 수 있는 API는 Auth 미들웨어를 사용하려고 함!

이제 User 모듈 및 Auth 미들웨어에서 JWT 패키지를 사용해야 함!

config/jwt.config.service.ts

UserService에서 JWT 패키지를 사용할 수 있게 JwtModule.register 함수를 통해서 주입하고 있습니다. 하지만, JwtModule의 secret 항목은 매우 민감한 항목이죠? 이게 혹여나 코드를 통해 노출이 되면 세션 하이재킹을 당할 수 있습니다. 해커가 유출된 비밀 키를 사용하여 자체 토큰을 생성하고 사용자 세션을 장악한 뒤에 민감한 데이터에 대한 액세스를 하거나 사용자 대신 작업을 수행할 수 있을거에요. 이게 소위 말하는 계정 해킹입니다.

또 하나는, 비밀 키가 유출되면 해당 키를 사용하여 발행된 모든 JWT 토큰의 진위를 더 이상 신뢰할 수 없습니다는 문제도 생깁니다. 비밀 키는 절대 사수해야 되겠다고 느껴지시죠? 그렇다면 이제 저번 TypeORM 시간에 배웠던 @nestjs/config 패키지를 사용하여 비밀 키를 캡슐화해봅시다. 이전 시간처럼 새로운 config 서비스 파일을 생성할거에요!

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

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]),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useClass: JwtConfigService,
      inject: [ConfigService],
    }),
  ],
  providers: [UserService],
  exports: [UserService],
  controllers: [UserController],
})
export class UserModule {}

user.service.ts(JWT 발급)

로그인 함수에서 JWT를 발급

import {
  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"], // 이외에도 다른 정보들이 필요하면 리턴해주면 됩니다.
    });
  }
}

JWT 검증

auth/auth.middleware.ts

JWT를 검증하는 Auth 미들웨어를 만들어 볼 것!

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} 를 보내면 AuthMiddleware는 JWT를 파싱하여 특정 유저임을 파악할 수 있습니다.

app.module.ts 1차 추가

AuthMiddleware는 모듈의 형태가 아니고 독립적인 서비스이기 때문에 JwtService를 DI하기 위해서는 AppModule에서 JwtService를 주입시킬 수 있도록 해야 합니다. 따라서, AppModule에도 JwtModule를 import하도록 하겠습니다.

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

user.controller.ts

nest g co user 명령어로 컨트롤러 추가

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

login, createUser 함수를 통해서 JWT를 발급받아야 updateUser를 호출할 수 있도록 할 것입니다.

이제 /user/update 에서는 올바른 JWT를 갖고있는 사용자만이 호출할 수 있도록 설정할 것입니다. 이것을 하기 위해서는 AppModule의 코드를 고쳐야합니다.

app.module.ts 2차 추가

export class AppModule implements NestModule { // NestModule 인터페이스 구현
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware) // 미들웨어 적용!
      .forRoutes({ path: "user/update", method: RequestMethod.PUT });
  }
}

@Module() 데코레이터를 설정하는 속성에는 imports, providers, exports 속성은 있으나 미들웨어를 설정하기 위한 속성은 없습니다.

대신 모듈 클래스의 configure()메서드를 사용하여 설정할 수 있습니다. Nest.js에서는 미들웨어를 포함하는 모듈은 NestModule 인터페이스를 구현해야 합니다. 구현한 후 consumer.apply로 적용할 미들웨어를 지정한 후 어떤 경로에 해당 미들웨어를 적용하는지 결정하면 됩니다!

위의 코드에서는 PUT /user/update에 해당되는 API에 AuthMiddleware를 적용하겠다는 것입니다! 이렇게 하면 유저 정보를 업데이트를 할 때 올바른 JWT를 넘겨야 유저 정보를 업데이트 할 수 있어요!

profile
개발자 꿈나무

0개의 댓글