회원가입 이메일 인증번호 Nodemailer + Redis

Seunghwa's Devlog·2022년 11월 3일
1

NestJs

목록 보기
3/15
post-thumbnail

회원가입 로직을 구현할 때 이메일로 인증번호를 보내서 확인시키고자 할 때의 로직을 구현해보자!

이메일로 인증번호를 보내기 위해 nodemailer라는 라이브러리를 사용하고자 했다. Gmail Oauth 2.0과 같이 사용해보자.

Gmail Oauth 2.0을 사용하기 위한 설정은 참고링크를 참고해주세요!

Nodemailer 사용하기

  • 라이브러리 설치
// npm
npm install --save nodemailer 

//yarn
yarn add nodemailer
  • 환경변수에 값 저장
GMAIL_ACCESS_TOKEN= Your Access Token
GMAIL_REFRESH_TOKEN= Your Referesh Token
GMAIL_CLIENT_ID= Your Client Id
GMAIL_CLIENT_SECRET= Your Client Secret
GMAIL_ID= 확인메일을 전송할 Gmail 계정

이 값들은 위 참고링크에서 설정을 하다보면 확인할 수 있다.

  • 코드 작성
// users.service.ts
async sendEmail(_id: string): Promise<boolean> {
    const user = await this.userRepository.findUserById(_id);

    const getRandomCode = (min, max) => {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min)) + min;
    };

    const randomCode = getRandomCode(111111, 999999);

    const transport = nodemailer.createTransport({
      service: 'Gmail',
      secure: true,
      auth: {
        type: 'OAuth2',
        user: process.env.GMAIL_ID,
        clientId: process.env.GMAIL_CLIENT_ID,
        clientSecret: process.env.GMAIL_CLIENT_SECRET,
        refreshToken: process.env.GMAIL_REFRESH_TOKEN,
        accessToken: process.env.GMAIL_ACCESS_TOKEN,
        expires: 3600,
      },
    });

    const sendResult = await transport.sendMail({
      from: {
        name: '인증관리자',
        address: process.env.GMAIL_ID,
      },
      subject: '내 서비스 인증 메일',
      to: [user.email],
      text: `The Authentication code is ${randomCode}`,
    });
    return sendResult.accepted.length > 0;
  }
// users.resolver.ts
import { Resolver, Query,  Args } from '@nestjs/graphql';
import { UsersService } from './users.service';

@Resolver()
export class UsersResolver {
constructor(
  private readonly usersService: UsersService,
) {}

@Query(() => Boolean)
async sendEmail(@Args() { _id }: ArgsUserDto): Promise<boolean> {
  return this.usersService.sendEmail(_id);
}
}
  • 실행

  • 결과

메일이 정상적으로 오는 것을 확인할 수 있다!

여기서 문제! 인증코드를 클라이언트가 확인해야 하는데 어떻게 관리할 것인가?

내가 했던 생각을 정리해 보자면,,

  • 인증코드를 DB에 저장하자
    처음엔 이렇게 하려고 했다. 근데 곰곰히 생각해 보면 인증코드는 회원가입 할 때 한번만 쓰이는 데이터고, 휘발성인데 굳이 DB에 저장해야 되나? 라는 의문점이 들었음

  • 인증코드를 해쉬값으로 변환해서 쿠키에 담아서 넘겨주자
    이 방법도 괜찮다고 생각했는데, 보안성에 있어서 너무 취약할 것 같았다.

  • redis를 사용하자
    휘발성 데이터인 인증코드를 cache 메모리를 사용하여 관리해보자고 생각했고, redis를 쓰기로 했다. Redis가 map 형태의 key.value 값을 지원하고 TTL 옵션을 통해 유효시간도 설정할 수 있으며, DB에 직접접근 하지 않기 때문에 성능의 이점도 존재한다.

최종적으로 redis를 사용하기로 결정!

그렇다면 NestJS에서 Redis를 어떻게 사용하는가?

- CacheModule 설정

  • 라이브러리 설치
npm install cache-manager
npm install -D @types/cache-manager
npm install redis
npm install cache-manager-redis-store
npm install -D @types/cache-manager-redis-store
  • Module import
import { CacheModule, Module } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';
import type { ClientOpts } from 'redis';

@Module({
  imports: [
    CacheModule.register<ClientOpts>({
      store: redisStore,
      host: process.env.HOST,
      port: process.env.REDIS_PORT,
      ttl: 180,
    }),
  ],
})

store는 다양한 store가 존재한다. 나는 공식문서를 보고 따라했기 때문에 redisStore를 사용했다.

ttl 옵션은 인증코드 유효시간을 3분으로 잡았기 때문에, 180으로 설정했다.

여기서 발생했던 문제!

https://docs.nestjs.com/techniques/caching
공식문서에 Different Store 부분 warning을 살펴보면,
"cache-manager-redis-store는 redis v4를 지원하지 않고, ClientOpts인터페이스가 존재하고 올바르게 작동하려면 최신 3.xx 주요 릴리스를 설치해야 합니다" 라고 적혀있다. 해당 issue에 대한 github를 살펴보면 같은 문제를 겪는 사람들이 많은 것을 확인할 수 있다.

이 문제를 해결하기 위해 라이브러리의 버전을 바꿔주었다.

npm install cache-manager@4.1.0
npm install cache-manager-redis-store@2.0.0
npm install redis@3.1.2

이렇게 변경을 하고나니 정상적으로 작동이 됨을 확인할 수 있었다.

- redis 로직 추가

import {
  CACHE_MANAGER,
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { UserRepository } from './repository/user.repository';
import * as nodemailer from 'nodemailer';
import { Cache } from 'cache-manager';

@Injectable()
export class UsersService {
  constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    private readonly userRepository: UserRepository,
  ) {}
  async sendEmail(_id: string): Promise<boolean> {
    const user = await this.userRepository.findUserById(_id);

    const getRandomCode = (min, max) => {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min)) + min;
    };

    const randomCode = getRandomCode(111111, 999999);

    const transport = nodemailer.createTransport({
      service: 'Gmail',
      secure: true,
      auth: {
        type: 'OAuth2',
        user: process.env.GMAIL_ID,
        clientId: process.env.GMAIL_CLIENT_ID,
        clientSecret: process.env.GMAIL_CLIENT_SECRET,
        refreshToken: process.env.GMAIL_REFRESH_TOKEN,
        accessToken: process.env.GMAIL_ACCESS_TOKEN,
        expires: 3600,
      },
    });

    if (randomCode) {
      await this.cacheManager.get(`${user.email}'s AuthenticationCode`);
    }
    await this.cacheManager.set(
      `${user.email}'s AuthenticationCode`,
      randomCode,
    );

    const sendResult = await transport.sendMail({
      from: {
        name: '인증관리자',
        address: process.env.GMAIL_ID,
      },
      subject: '내 서비스 인증 메일',
      to: [user.email],
      text: `The Authentication code is ${randomCode}`,
    });
    return sendResult.accepted.length > 0;
  }
}

여기서 주의할 점!
cacheManager의 type인 Cache를 ‘cache-manager’에서 import 해야 한다.

  • 실행

이 에러가 발생하는 이유 -> redis sever를 실행을 안했기 때문!

redis server 실행 명령어

redis-server
  • 정상적으로 redis server가 실행된 화면

  • 실행결과 확인
    redis localhost:6379 접속

redis-cli

  • redis local에 key.value 제대로 저장되어 있는지 확인
    메일 전송 확인

  • redis key 조회

keys *

  • key에 대한 value 조회
get keyName

  • ttl 옵션이후 key.value 만료여부 조회

    -> 180초가 지난 이후 key값이 empty인 것을 확인할 수 있음

메일로 전송된 인증번호와 redis local에 저장된 value값이 일치하는 것을 확인할 수 있다!

결론

redis는 처음 써봤는데, 아직 익숙하지 않은 것 같다. 추후 학습을 통해서 익숙해지도록 해야겠다. 익숙해지고나면 Amazon ElastiCache for Redis 서비스를 사용해서도 구현해보도록 해야겠다!

참고링크
https://velog.io/@13circle/Nodemailer-X-Gmail-OAuth-2.0
https://docs.nestjs.com/techniques/caching

profile
에러와 부딪히고 새로운 것을 배우며 성장해가는 과정을 기록합니다!

0개의 댓글