회원가입 시 이메일 인증 구현

nayu1105·2023년 9월 22일
1

Instagram clone

목록 보기
1/1
post-thumbnail

최근 Instagram clone 프로젝트하며 회원가입 시 인증코드를 확인하는 절차를 만들었다.

이메일 정보를 입력하고 인증코드 보내기 버튼이 있으면 호출되는 api, 인증코드를 확인하는 api 이렇게 두가지를 구현했다.

회원가입 시 이메일 인증 구현

1. Gradle, yml, 구글 계정 설정

먼저 gradle 설정을 해주었다.

depencencies에 다음을 추가해주었다.

spring에서 mail을 보내기 위한 라이브러리와, 메일에 적힐 내용을 html을 작성할 thymeleaf 라이브러리를 추가하였다.

dependencies {
	...
    // mail
    implementation 'org.springframework.boot:spring-boot-starter-mail:3.1.2'

    // thymeleaf
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
    ...
    }

yml 도 설정해주었다.

  mail:
    host: smtp.gmail.com
    port: 587
    username: ${SPRING_MAIL_USERNAME}
    password: ${SPRING_MAIL_PASSWORD}
    properties:
      mail:
        smtp:
          auth: true
          timeout: 5000
          starttls:
            enable: true

이때 SPRING_MAIL_USERNAME 와 SPRING_MAIL_PASSWORD에는 메일을 송신할 계정을 적어야한다.

이를 위해 구글 계정 설정이 필요했다.

Google Gmail SMTP 설정 방법 및 메일 전송

이를 참고하여 구글 계정을 설정한 후, gmail 설정에서 아래와 같이 바꾸어 주었다.

2. 인증 코드 발송 API 구현

EmailService 를 작성하였다. sendEmail 함수를 호출하면 emailRequest의 email에 메일을 보내도록 구현하였다.

  1. sendEmail 호출
  2. authNum 생성
  3. redis에 authNum 저장 (유효기간 5분)
  4. request의 email로 authNum이 들어간 HTML 전송
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {

    private final JavaMailSender javaMailSender;
    
    private final SpringTemplateEngine templateEngine;

    private final EmailMapper emailMapper;

    private final RedisUtils redisUtils;

    private final long expire_period = 1000L * 60L * 30; // 30분

    @Async
    public void sendMail(EmailRequest emailRequest, String type) {
        String authNum = createCode();

        MimeMessage mimeMessage = javaMailSender.createMimeMessage();

        EmailMessage emailMessage = emailMapper.dtoToVo(emailRequest);

        try {
            MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
            mimeMessageHelper.setTo(emailMessage.getTo()); // 메일 수신자
            mimeMessageHelper.setSubject(emailMessage.getSubject()); // 메일 제목
            mimeMessageHelper.setText(setContext(authNum, type), true); // 메일 본문 내용, HTML 여부
            javaMailSender.send(mimeMessage);

            log.info("code : {}, message : {}", HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase());

            Date now = new Date();
            redisUtils.setDataExpire(RedisCode.AUTH_NUM.getCode() + emailMessage.getTo(), authNum, expire_period);
        } catch (MessagingException e) {
            log.error(e.getMessage());
            log.info("code : {}, message : {}", HttpStatus.INTERNAL_SERVER_ERROR.value(),
                HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
            throw new RuntimeException(e);
        }
    }

    // 인증번호 및 임시 비밀번호 생성 메서드
    public String createCode() {
        Random random = new Random();
        StringBuffer key = new StringBuffer();

        for (int i = 0; i < 8; i++) {
            int index = random.nextInt(4);

            switch (index) {
                case 0:
                    key.append((char) ((int) random.nextInt(26) + 97));
                    break;
                case 1:
                    key.append((char) ((int) random.nextInt(26) + 65));
                    break;
                default:
                    key.append(random.nextInt(9));
            }
        }
        return key.toString();
    }

    // thymeleaf를 통한 html 적용
    public String setContext(String code, String type) {
        Context context = new Context();
        context.setVariable("code", code);
        return templateEngine.process(type, context);
    }

}

EmailMessage는 다음과 같다.

@Getter
@Builder
@AllArgsConstructor
public class EmailMessage {

    private String to;
    private String subject;
    private String message;

}

AuthController에 아래 api를 추가하였다.

    @PostMapping("/send-mail")
    public ResponseEntity<EmptyResult> sendMail(@RequestBody @Valid EmailRequest emailRequest) {
        emailService.sendMail(emailRequest, "email");
        log.info("sendMail code : {}, message : {}", HttpStatus.OK, HttpStatus.OK.getReasonPhrase());
        return ResponseDto.of(HttpStatus.OK, HttpStatus.OK.getReasonPhrase());
    }

그리고 resources/template에 email.html을 추가했다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<body>
<div style="margin:100px;">
  <h3> Instagram </h3>
  <h1> 이메일 인증번호 안내 </h1>
  <br>
  <p> 본 메일은 Instagram의 회원가입을 위한 이메일 인증입니다.</p>
  <p> 아래의 [이메일 인증번호]를 입력하여 본인확인을 해주시기 바랍니다.</p>
  <br>

  <div align="center" style="background-color: rgb(0, 0, 0, 0.08); font-family:verdana;">
    <div style="font-size:130%" th:text="${code}"> </div>
  </div>
  <br/>
</div>

</body>
</html>

3. 인증코드 확인 API 구현

VerifyMailRequest내용은 다음과 같다.

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class VerifyMailRequest {

    private String email;
    private String authNum;
}

AuthService에 아래의 내용을 추가하였다.

  1. redis 사용자의 인증코드를 가져온다.
  2. 인증코드와 request의 인증코드가 같은지 판별하여 response를 생성한다.
    @Transactional
    public VerifyMailResponse verifyMail(VerifyMailRequest request) {
        String authNum = redisUtils.getData(RedisCode.AUTH_NUM.getCode() + request.getEmail());
        return VerifyMailResponse.builder().verify(authNum.equals(request.getAuthNum())).build();
    }

AuthController에 아래 api를 추가하였다.

    @PostMapping("/verify/mail")
    public ResponseEntity<SingleResult> verifyMail(@RequestBody VerifyMailRequest verifyMailRequest) {
        VerifyMailResponse response = authService.verifyMail(verifyMailRequest);
        log.info("verifyMail code : {}, message : {}", HttpStatus.OK, HttpStatus.OK.getReasonPhrase());
        return ResponseDto.of(HttpStatus.OK, HttpStatus.OK.getReasonPhrase(), response);
    }

4. Docker, Postman으로 테스트

Docker로 local에 redis를 올리고, postman으로 api를 호출해서 테스트 해보았다.

성공을 확인하러 네이버 메일함을 가보았다.

잘 도착했음을 확인했다.

이를 가지고 이메일 인증을 해보자.

  1. 잘못된 인증번호

  2. 올바른 인증번호

원래는 인증번호가 틀리면 badrequest를 줄까하다가, boolean 값으로 verify변수에 담아 status 200 으로 주도록 구현했다.

5. 회고

이메일 인증을 한번은 구현해보고 싶었는데, 생각보다 금방 구현했다!

구현은 금방인데, request, response를 어떻게 주고 받을 지 많이 고민한 것 같다.

이전에 프론트 측이랑 이야기하다가 BadRequest가 개발자모드(F12)에 쌓이는게 싫다는 의견도 있고,
true, false를 주면 버튼 비활성화를 바로 사용할 수 있을 거같기도 해서 이렇게 구현하긴 했는데
결국 잘못된 값이니 또 BadRequest가 맞긴하고...

고민하다 결국 boolean으로 구현했다.

지금보니 BadRequest를 주는게 나은 거 같기도하고,,..

정답은 없겠지만 대부분 어떻게 하는지 더 찾아봐야겠다.

그리고 이메일 인증을 하면서 배우고 싶은 게 생겼다.

구독한 사람들에게 어플리케이션 소식을 주기적을 보내는 로직도 구현해보고 싶어졌다.

위에 메일들은 내가 구독해서 보는 뉴스레터들이다.

매주 혹은 매일 메일을 보내준다.

예상되는 고려할 점은

  1. 구독한 사람들 모두 빠짐없이 메일을 받아야 하기에 순차적으로 보내야할 듯 하다
  2. 모든 사람들에게 전송되기 위해 하나의 서버에서 부담한다면 매우 오래걸리 것 같다. 빠르게 보낼 수 있는 다른 방법을 찾아보자.
  3. 자동으로 특정 시간에 모든 사람들에게 전송되기 시작해야 한다. spring schedule 같은 게 사용될 것 같다.

이정도 일 것 같다.

다음 번에는 뉴스레터 구독, 구독 취소, 특정 시간에 뉴스레터 보내기 등을 구현해봐야겠다.

0개의 댓글