Spring Boot : 메일 인증 기능 구현하기 (2)

이민호·2024년 6월 14일
0
post-thumbnail

지난 포스팅에서 Gmail SMTP에 관련된 설정을 완료했다.

이제 우리 서버에서 인증코드를 담은 메일을 전송하고, 그것으로 어떻게 사용자를 인증할 것인지에 대해 생각해 봐야한다.

메일 인증 기능 흐름

메일 전송

사용자 : 메일 입력 -> 서버 : 입력된 메일로 코드 전송

메일 검증

사용자 : 전송된 코드를 서버에 입력 -> 서버 : 입력 받은 코드와 발송한 코드가 일치하는 지 검증

고려사항

  • 사용자에게 보낸 인증 코드를 검증하기 위해서는 서버에 발송된 코드를 저장 할 필요가 있다.
  • 인증 코드는 한 가지만 유효하게 한다. : 재발송시 기존 코드는 무효화

기능 구현

Mail Entity

@Getter
@NoArgsConstructor
@Entity
@Table(name = "mail")
public class Mail extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;
	// 인증 여부
    @Column
    private boolean status;
	// 메일 주소
    @Column
    private String email;
	// 인증 코드
    @Column
    private String code;
	// 사용자와 1대1 연결
    @OneToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @Builder
    public Mail(User user) {
        this.user = user;
        this.email = user.getEmail();
        this.code = "";
        this.status = false;
    }
	// 인증 코드 업데이트
    public void mailAddCode(String code) {
        this.code = code;
    }
  • id: 메일 엔티티의 고유키
  • status: 인증 여부 상태를 나타내는 필드
  • email: 메일을 발송한 메일 주소
  • code: 인증 코드
  • User: 인증 대상 사용자

MailController

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users/email")
public class MailController {
    private final MailService mailService;
	// 메일 전송
    @PostMapping("/send")
    public ResponseEntity<String> sendMail(@RequestBody MailRequestDto requestDto) throws MessagingException {
        mailService.sendMail(requestDto);
        return ResponseEntity.ok(requestDto.getEmail() + "로 인증코드가 발송되었습니다.");
    }
	// 메일 검증
    @PostMapping("/verify")
    public ResponseEntity<String> verifyMail(@RequestBody VerifyRequestDto requestDto) {
        mailService.verifyMail(requestDto);
        return ResponseEntity.ok("입력하신 메일 " + requestDto.getEmail() + "이 정상적으로 인증되었습니다.");
    }
}

MailService

sendMail


    public void sendMail(MailRequestDto requestDto) {
        Mail mail = checkAndSaveMail(requestDto.getEmail());
        MimeMessage emailForm = createMailForm(mail);
        javaMailSender.send(emailForm);

    }

MailRequestDto 객체를 매개변수로 받는 sendMail 메서드

MailRequestDto.java
// MailRequestDto.java
@Getter
@NoArgsConstructor
public class MailRequestDto {
    private String email;

    @Builder
    public MailRequestDto(String email) {
        this.email = email;
    }
}

requestDto 객체에서 메일 주소를 받아서 checkAndSaveMail 메서드를 수행

checkAndSaveMail
private Mail checkAndSaveMail(String email) {
        User user = getUserByEmail(email);
        Optional<Mail> checkMail = mailRepository.findByEmail(user.getEmail());
        checkMail.ifPresent(mailRepository::delete);
        Mail mail = Mail.builder()
                .user(user)
                .build();

        return mailRepository.save(mail);
    }

저장된 사용자 정보 조회 (회원가입 도중이지만 임시 상태로 저장되어 있음)
메일 주소를 받아서 MailRepository에서 같은 메일 주소를 조회 -> 존재하면 메일 엔티티 삭제
새로운 메일 엔티티 생성 후 저장

사용자에게 보낼 메일 형태 생성 후 전송

createMailForm
private MimeMessage createMailForm(Mail mail) {
      String code = createCode();
      mail.mailAddCode(code);

      MimeMessage message = javaMailSender.createMimeMessage();

      try {
          MimeMessageHelper messageHelper = new MimeMessageHelper(message, true);
          String senderEmail = "noreply_88@gmail.com";
          messageHelper.setFrom(senderEmail);
          messageHelper.setTo(mail.getEmail());
          messageHelper.setSubject("[88하게] 이메일 인증 번호 발송");

          String body = "<html><body style='background-color: #000000 !important; margin: 0 auto; max-width: 600px; word-break: break-all; padding-top: 50px; color: #ffffff;'>";
          body += "<h1 style='padding-top: 50px; font-size: 30px;'>이메일 주소 인증</h1>";
          body += "<p style='padding-top: 20px; font-size: 18px; opacity: 0.6; line-height: 30px; font-weight: 400;'>서비스 사용을 위해 회원가입 시 고객님께서 입력하신 이메일 주소의 인증이 필요합니다.<br />";
          body += "하단의 인증 번호로 이메일 인증을 완료하시면, 정상적으로 서비스를 이용하실 수 있습니다.<br />";
          body += "<div class='code-box' style='margin-top: 50px; padding-top: 20px; color: #000000; padding-bottom: 20px; font-size: 25px; text-align: center; background-color: #f4f4f4; border-radius: 10px;'>" + code + "</div>";
          body += "</body></html>";
          messageHelper.setText(body, true);
      } catch (MessagingException e) {
          e.printStackTrace();
      }
      return message;
  }

createCode 메서드로 임의의 문자로 이루어진 인증 코드 생성

createCode
// 이메일 인증코드 생성
    private String createCode() {
        int numberzero = 48; // 0 아스키 코드
        int alphbetz = 122; // z 아스키 코드
        int codeLength = 8; // 인증코드의 길이
        Random rand = new Random(); // 임의 생성

        return rand.ints(numberzero, alphbetz + 1)
                .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) // 숫자와 알파벳만 허용
                .limit(codeLength)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();
    }

생성된 인증 코드를 메일 엔티티에 추가

Spring boot에 포함된 MimeMessage class를 사용하여 이메일 구조 생성 후 반환

  • 생성된 메일 폼

verifyMail

public void verifyMail(VerifyRequestDto requestDto) {
        LocalDateTime now = LocalDateTime.now(); // 유효시간 체크를 위한 현재시간 체크
        User user = getUserByEmail(requestDto.getEmail());
  
  		// 해당하는 메일주소가 없는 경우.
        Mail mail = mailRepository.findByEmail(user.getEmail()).orElseThrow(
                () -> new MailServiceException("잘못된 이메일입니다.")
        );
		
  		// 인증 유효시간 180초가 지난 경우
        LocalDateTime timeLimit = mail.getCreatedAt().plusSeconds(EXPIRED_TIME);
        String targetCode = mail.getCode();
        String code = requestDto.getCode();

        if (now.isAfter(timeLimit)) {
            throw new MailServiceException("만료된 인증코드입니다.");
        }
		
  		// 발송된 코드와 입력받은 코드가 일치하는 경우
        if (targetCode.equals(code)) {
            user.updateStatusVeryfied(); // 사용자의 상태를 임시 -> 정상으로 변경
        }
    }
profile
둘뺌

0개의 댓글