[밍글] SMTP + ElastiCache(Redis)로 이메일 인증코드 구현하기

KIM TAEHYUN·2023년 6월 6일
1
post-thumbnail

유학생들을 위한 커뮤니티 앱 "밍글"을 개발하며 학교 인증을 고민하던 중 학교 이메일로 재학생을 인증하기로 결심했습니다.
이에 회원가입 과정에서 학교 이메일로 인증코드를 보내 인증하는 방식을 Spring Boot + SMTP(Google Workspace) + ElastiCache(Redis)로 구현한 과정을 적어보고자 합니다.

1. 스프링부트 설정

  • 우선 build.gradle 파일에 dependency를 추가해줍니다.
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
  • 이메일 발송을 위해서 spring-boot-starter-mail dependency 추가

  • 인증번호 유효시간 동안 Redis에 저장하기 위한 spring-boot-starter-data-redis dependency 추가

  • application.yml 설정

spring:
	mail:
	//   host, port는 google에서 정해준 값
	    host: smtp-relay.gmail.com //밍글 도메인에서 보내기에 relay 필요
	    port: 587 //for TLS 
	//  Gmail 계정 정보
	    username: 이메일@domain.com //(밍글 google workspace 도메인)
	    password: ******* // 1
	    properties:
	      mail.smtp.auth: true
				mail.smtp.starttls.enable: true

	// ElastiCache(Redis) 설정
	redis:
    	host: elastiCache 클러스터 주소
    	port: 6379

주석 1: 원래라면 해당 이메일 계정의 비밀번호를 적어야 했지만, 구글이 2022년 5월 30일부터 이를 막아 2단계 인증을 통해 발급받은 앱 비밀번호를 넣어줘야 합니다.

아래 SMTP 설정에서 앱 비밀번호에 대해 더 자세히 설명하겠습니다.

2. SMTP 설정

📧 밍글 전용 도메인을 만들어 google workspace 설정 필요

2.1 구글 2단계 인증 설정

앱 비밀번호를 발급하는 순서는 다음과 같습니다.

  1. https://myaccount.google.com/u/1/security 링크 또는 Google 계정 관리 → 보안 탭에 접속하여 2단계 인증을 사용하도록 합니다.
  2. 밑으로 내려 앱 비밀번호를 생성합니다.
    • 앱 선택 : 메일
    • 기기 선택 : 기타(맞춤 이름)

  1. 생성 완료
  • 생성된 비밀번호는 다시 확인할 수 없으니 잘 적어놓은 후 이 비밀번호를 application.yml에 password 부분에 넣어줍니다.

2.2 SMTP 릴레이 활성화

Google Workspace의 SMTP 릴레이는 관리 콘솔에서 활성화해야 동작하기에 관련 설정을 해보겠습니다.

  1. Google Workspace 관리콘솔 (https://admin.google.com/u/1/) 에서 [Apps] - [Google Workspace] - [Gmail] - [Routing] 으로 접속합니다.

2 . SMTP Relay Service에 아래의 설정 옵션 표를 참고해 다음과 같이 규칙을 추가해주었습니다.

이렇게 Google Workspace와 SMTP 설정을 마쳤습니다.

이제 실제 인증번호 전송 코드를 살펴보겠습니다.


3. 어플리케이션 로직

이메일 인증코드 전송 API를 작성해보겠습니다.

  • AuthController.java
@PostMapping("sendcode") // 이메일 인증코드 전송 API
public BaseResponse<String> sendCode(@RequestBody PostEmailRequest req) {
  try {
      if (req.getEmail().isEmpty()) {
          return new BaseResponse<>(EMAIL_EMPTY_ERROR);
      }
      if (!isRegexEmail(req.getEmail())) { //이메일 형식(정규식) 검증
          return new BaseResponse<>(EMAIL_FORMAT_ERROR);
      }
      authService.sendCode(req.getEmail());
      return new BaseResponse<>("인증번호가 전송되었습니다.");
  } catch (BaseException e) {
      return new BaseResponse<>(e.getStatus());
  }
}

이메일 정규식 검증을 해주고 문제가 없다면 이메일을 전달하며 서비스 단을 호출합니다.

  • AuthService.java
@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class AuthService {

  	private final JavaMailSender javaMailSender; //MIME을 지원하는 MailSender 구현체
  	private final RedisUtil redisUtil;
	@Value("${spring.mail.username}")
  	private String from;

  	// 1.4.1인증번호 생성
	@Transactional
	public void sendCode(String email) throws BaseException {
	    Random random = new Random(); 
	    String authKey = String.valueOf(random.nextInt(888888) + 111111);
	    sendAuthEmail(email, authKey); 
	}
	
	// 1.4.2인증번호 이메일 전송
	private void sendAuthEmail(String email, String authKey) throws BaseException{
	  String subject = "Mingle의 이메일 인증번호를 확인하세요";
	  String text = "\n\n인증번호는 " + authKey + " 입니다.";
	  try {
	      MimeMessage mimeMessage = javaMailSender.createMimeMessage();
	      MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8");
	      helper.setFrom(from); //yml에서 @Value로 가져온 보내는 송신자 이메일 주소
	      helper.setTo(email); //인자로 받은 이메일 수신자 주소
	      helper.setSubject(subject); //제목 
		//helper.setText(text, true); //템플릿 사용으로 주석처리

		//템플릿에 전달할 데이터 설정
		Context context = new Context();
		context.setVariable("authKey", authKey); //Template에 전달할 데이터(authKey) 설정
		//메일 내용 설정 : 템플릿 프로세스
		String html = springTemplateEngine.process("index", context);
        helper.setText(html, true); 
        helper.addInline("image", new ClassPathResource("templates/images/image-1.jpeg"));

		javaMailSender.send(mimeMessage);
	  	} catch(MessagingException e) {
	      throw new BaseException(EMAIL_SEND_FAIL);
		}	
		//Redis에 3분동안 인증코드 {email, authKey} 저장
        try {
            redisUtil.setDataExpire(email, authKey, 60 * 3L);
        } catch (Exception e) {
            throw new BaseException(DATABASE_ERROR);
        }
	}
  }

작성한 메서드를 자세히 살펴보겠습니다.

1.4.1 sendCode(String email)

  • 인증코드로 사용될 111111부터 999999 사이의 난수 authKey를 생성합니다.
  • 생성한 authkey를 request 의 email과 함께 endAuthEmail 의 인자로 보내주며 호출합니다.

1.4.2 sendAuthEmail (String email, String authkey)

  • 인자로 받은 이메일로 authKey, 인증코드를 보내주는 메서드입니다.
  • MimeMessageHelper : 스프링에서 제공하는 헬퍼 객체이며, HTML 레이아웃, 이미지 삽입, 첨부파일 등 MIME 메시지를 생성할 수 있습니다.
    • helper.setText() - 메일 본문 작성
      • 첫 번째 파라미터로 HTML을 전달합니다. (text 전달 가능)
      • 두 번째 파라미터에 HTML 사용여부를 전달합니다.

이메일 템플릿 설정

여기까지만 진행해도 이메일을 보낼 수 있지만, 추가적으로 이메일을 이미지와 템플릿을 이용해 꾸며주었습니다.

1.4.2 sendEmail 메서드 중 템플릿 관련 코드를 보겠습니다.

//템플릿에 전달할 데이터 설정 (타임리프 설정)
Context context = new Context();
context.setVariable("authKey", authKey); //Template에 전달할 데이터(authKey) 설정
//메일 내용 설정 : 템플릿 프로세스 
String html = springTemplateEngine.process("index", context); //index.html
helper.setText(html, true);  
helper.addInline("image", new ClassPathResource("templates/images/image-1.jpeg"));
  • src/main/resources/templates 경로 아래에 html 이메일 템플릿 테마인 index.html을 넣어주었습니다.
  • 또한, 이미지를 전송하기 위해 cid를 이용하여 이미지를 html안에 임베딩하여 보내는 방식을 사용했습니다.

따라서 전송할 html (index.html) 양식 안의 img 경로를 다음과 같이 설정해둔 후

<img src='cid:image'>

이메일 전송 전 addInline 메소드로 해당 cid를 적용했습니다.

helper.addInline("image", new ClassPathResource("templates/images/image-1.jpeg"));
  • cid의 이름과 addInline의 첫 번째 인자를 서로 매칭해줍니다.
  • 두 번째 인자에는 실제 저장되어 있는 이미지 경로를 넣어주면 됩니다. Resource implementation for class path resources 인 ClassPathResource 클래스를 이용해 absolute path within the class path를 작성해주었습니다.

Redis 설정

이메일 템플릿 구현이 끝났으니, 아래 Redis 부분을 보겠습니다.

  • redisUtil.setDataExpire(email, authKey, 60 * 3L)

Redis(REmote Dictionary Server)는 NOSQL, 비 관계형 데이터베이스입니다.  KEY와 VALUE 구조로 이루어져 있으며 처리가 빠르고 유효 시간을 정해 데이터가 남지 않도록 할 수 있습니다. 인증번호를 3분 동안만 저장하고 소멸시키기 위해 Redis를 도입했습니다.

  • Redis를 사용하기 위해 application.yml 에 아래 코드를 추가해줍니다.
  • 기존에는 ec2에 서버를 올려 ec2에 Redis를 설치해서 쓸 수 있었지만, ECS Fargate로 전환하면서 AWS에서 제공하는 ElastiCache(Redis)를 도입했습니다.
  • ECS Fargate와 같은 VPC 내부에 AWS ElastiCache(Redis)를 만들어 연결해주었습니다. (ElastiCache는 같은 VPC 내부에서만 접속이 가능합니다.)
redis:
    host: AWS ElastiCache Redis 클러스터 리더 엔드포인트 //127.0.0.1 (local)
    port: 6379
  • RedisUtil.java
    Redis 에는 다양한 메서드가 있지만, 인증번호 구현을 위한 setDataExpire 메서드만 보겠습니다.
  • setDataExpire(String key, String value, long duration) : {key, value}가 특정 유효 시간 동안만 저장되도록 함
@Service
@RequiredArgsConstructor
public class RedisUtil {

    private final StringRedisTemplate stringRedisTemplate;

    public void setDataExpire(String key, String value, long duration) {
        ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
        Duration expireDuration = Duration.ofSeconds(duration);
        valueOperations.set(key, value, expireDuration);
    }
}
  • ValueOperations<K,V>: Redis operations for simple (or in Redis terminology 'string') values 를 정의해놓은 인터페이스
  • valueOperations.set(K key, V value, Duration timeout) : Set the value and expiration timeout for key

이렇게 SMTP와 ElastiCache(Redis)를 이용해 학교 인증용 이메일 인증코드 전송 기능을 구현해보았습니다.
인증코드 API를 호출하면, 아래 사진과 같이 이메일이 오는걸 확인할 수 있습니다.

감사합니다.

1개의 댓글

comment-user-thumbnail
2023년 9월 4일

이메일이 학교 이메일인지 아닌지 확인하는 처리도 하셨나요??

답글 달기