회원가입 로직을 구현할 때 이메일로 인증번호를 보내서 확인시키고자 할 때의 로직을 구현해보자!
이메일로 인증번호를 보내기 위해 nodemailer라는 라이브러리를 사용하고자 했다. Gmail Oauth 2.0과 같이 사용해보자.
Gmail Oauth 2.0을 사용하기 위한 설정은 참고링크를 참고해주세요!
// 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를 사용하기로 결정!
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
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
이렇게 변경을 하고나니 정상적으로 작동이 됨을 확인할 수 있었다.
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 *
get keyName
메일로 전송된 인증번호와 redis local에 저장된 value값이 일치하는 것을 확인할 수 있다!
redis는 처음 써봤는데, 아직 익숙하지 않은 것 같다. 추후 학습을 통해서 익숙해지도록 해야겠다. 익숙해지고나면 Amazon ElastiCache for Redis 서비스를 사용해서도 구현해보도록 해야겠다!
참고링크
https://velog.io/@13circle/Nodemailer-X-Gmail-OAuth-2.0
https://docs.nestjs.com/techniques/caching