[Nest.js] Cookie 관리하기

HoonDong_K·2023년 8월 13일
0

[project #3] Planner Bot

목록 보기
3/4
post-thumbnail
post-custom-banner

우리는 다음의 필요성을 통해 사용자의 ChatGpt 이용 횟수를 제한하고자 하였다.

  • OpenAI 요금은 직접 지불한다.
  • 사용자의 무분별한 요청은 요금을 기하급수적으로 상승시킨다.
    ( 채팅 로그와 함께 요청이 들어가기 때문에 )
  • 프로젝트의 수익성이 없다.

그렇기 때문에, 우리는 사용자의 사용 횟수를 5회로 제한하고 몇일 뒤 제한이 해제되어 다시 이용할 수 있게 하기로 하였다.

🙅‍♀️서비스 이용 제한

문제점

서비스 이용을 제한하는 방법은 다양하겠지만 우리가 제공하는 서비스는 다양한 방법을 구사할 정도의 환경을 제공하지 못하였다.

  • DB 없음
  • 회원가입 / 로그인 서비스 없음

서비스 이용을 제한하기 위해 가장 먼저 고려해야할 것은 사용자를 구별하는 것이다. 하지만 우리는 사용자 정보를 받아올 회원 가입 서비스도, 정보를 저장할 DB도 구축하지 않았기 때문에 다른 방안을 고안할 필요가 있었다.

사용자 구별 방식

그렇게 고안해낸 방식은 사용자의 IP를 조회하는 것이다. IP 조회는 다행히도 무료로 API 요청을 통해 조회해올 수 있었고 이를 토큰화 하여 쿠키에 저장시키는 방식을 채택하였다.

  1. 사용자가 서비스를 이용할 때, IP 주소와 함께 서버로 요청을 보낸다.
  2. 서버에서 IP 주소를 JWT를 통해 토큰 발급을 하여 클라이언트 cookie에 저장한 후, GPT 응답을 보낸다.
  3. 이 후, 클라이언트에서 서비스 사용 횟수를 계산하여 5회가 초과되었을 경우, cookie에 저장된 토큰을 삭제한다.
  4. 사용자는 cookie에 토큰이 없을 경우, 서비스 이용이 제한된다.

하지만 이러한 점은 몇 가지 문제점을 발생시켰다.

  1. 클라이언트가 홈페이지를 나갔다가 재접속하였을 때, 서버에서는 서비스 이용이 제한된 사용자인 지 구별할 수 없다. ( DB가 없어, IP 주소 저장도 불가 )
  2. 4회 이용 시, 새로고침을 하면 횟수가 초기화 된다.

그래서 방식을 반대로 적용하기로 하였다. 처음부터 cookie에 토큰이 저장되지 않았을 경우에만 서비스를 이용할 수 있으며, 5회를 초과할 경우에 cookie에 토큰을 저장하게 된다면 사용자가 재접속을 하더라도 cookie에는 여전히 토큰이 남아있게 된다.

🙆‍♀️ 서비스 이용제한 구현

Nest에서 JWT 서비스를 사용하려면@nestjs/jwt를 설치해야 한다.

$ npm install --save @nestjs/jwt
//gpt.service.ts

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class GptService {
  constructor(
    private configService: ConfigService,
    private jwtService: JwtService,
  ) {}
  
  generateToken(payload: { ip: string }) {
    const token = this.jwtService.sign(payload, {
      secret: this.configService.get<string>('JWT_SECRET'),
    });
    return { token };
  }
}

configService 에 대한 설정은 이 글을 참고해주세요.

generateToken 함수는 클라이언트에게 IP주소를 파라미터로 받아오면 JWT 토큰을 반환한다.

//gpt.controller.ts

  @Post('/token')
  generateToken(@Body() toeknDto: TokenDto, @Res() res: expRes) {
    const { token } = this.gptService.generateToken(toeknDto);
    res.cookie('gpt_token', token, {
      domain: '.planbot.click',
      secure: true,
      maxAge: 7 * 24 * 60 * 60 * 1000, //7d,
      sameSite: 'none',
      path: '/',
    });
    return res.send({ token });
  }

발급된 토큰은 클라이언트 cookie에 저장시켜준다. 참고로 @Res() res: expRes 부분은 Responseexpress@nestjs/common에서 중복으로 import되기 때문에, expressResponseexpRes로 변경하여 import 해주었다.

+) 응답을 express 타입으로 불러왔기 때문에, 기존에 nest에서 하던대로 return으로 반환하는 것이 아닌, res.send로 반환해주어야 한다.

쿠키 설정

쿠키의 옵션값들은 여러가지가 있는데, 생각보다 간단하면서도 어려운 설정이다.

  • domain
    쿠키에 접근할 수 있는 도메인을 설정한다. 현재 사용하고 있는 도메인은
    www.planbot.click 이며 서버는 api.planbot.click를 사용하고 있기 때문에, 루트 도메인 값인 .planbot.click을 설정해두었다.

  • secure
    https에서 쿠키를 사용하기 위해 설정하였다.

  • maxAge
    쿠키의 만료는 7일로 설정하였다.

  • sameSite
    secure를 true로 설정하고 쿠키의 사용범위를 넓히기 위해서 설정하였다

  • path
    쿠키에 접근 가능한 경로이다.

자세한 문서는 여기를 참고하세요

참고로 프로젝트를 진행하며 서버와 클라이언트 간 쿠키를 저장해보며 테스트를 해볼 것이다.

우리 또한 쿠키 설정하는 과정에서 여러 테스트를 진행해보았지만, 쿠키 설정도 완벽하고 요청에도 쿠키가 담겨있으며, 서버에도 쿠키가 잘 전달되지만 정작 개발자 도구에서 저장이 안되는 것을 확인하였다.

이 경우에는 꼭 쿠키를 한 번 다 제거해보고 다시 시도해보도록 하자.
( 별 것도 아니지만 꽤 오랜 시간 걸렸다.

Guard를 통해 서비스 제한

쿠키에 토큰이 성공적으로 저장이 되었다면, 클라이언트의 매 요청마다 쿠키에 토큰이 담겨서 전달될 것이다.

그렇다면 우리는 이 토큰의 여부에 따라 서비스 제한을 시켜야 한다.

//token.auth.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class TokenGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const { gpt_token } = request.cookies;

    if (!gpt_token) return true;
    else {
      throw new HttpException(
        '5회 무료 이용이 끝났습니다.',
        HttpStatus.FORBIDDEN,
      );
    }
  }
}

guard를 설정한 api에 요청이 들어올 때마다, request.cookies에 토큰으로 저장한 gpt_token이 포함되어 있는 지 확인한 후, 토큰이 발견될 경우 메세지를 날려주며 403 Forbidden 을 응답한다.

profile
개발을 잘하고 싶은 개발자
post-custom-banner

0개의 댓글