프로젝트를 진행하면서 이메일 인증 로직과 임시 비밀번호 전송 로직을 구현할 필요성이 생겨서 Spring으로 메일 보내는 방법에 대해 찾아보고 구현하게 되었습니다. 그 중 이메일 인증 로직 중심으로 구현한 부분에 대해 설명하도록 하겠습니다.
메일 전송을 위해 필요한 API는 다음과 같습니다.
메일 전송을 위해 필요한 로직은 다음과 같습니다.
임시 인증 번호를 보내면 인증 번호가 일치하는지 여부를 확인하기 위해서 인증 번호와 이메일 정보를 어딘가에 저장할 필요성이 생겼습니다.
기존 DB에 저장할지, 아니면 Redis 같은 인메모리 DB를 사용하여 저장할지 고민하다가 Redis를 사용하기로 결정했습니다.
key-value 쌍으로 데이터를 관리할 수 있는 인메모리 DB
메모리에 위치해 있어 처리 속도가 빠르기도 하고, 인증 확인을 위한 인증 번호와 이메일은 영구 저장될 필요가 없고, TTL 설정을 하면 일정 시간이 지나면 알아서 삭제되기 때문에 편하게 데이터를 관리할 수 있다고 생각했습니다.
저는 이 블로그를 참고하여 구현하였습니다 :)
Gmail 보안 2단계 설정 후 앱 비밀번호를 생성합니다.
생성한 앱 비밀번호는 Gmail의 비밀번호를 대신해 사용해주면 됩니다.
저는 gradle로 프로젝트를 진행했기 때문에 아래 라이브러리를 추가해주었습니다.
implementation 'org.springframework.boot:spring-boot-starter-mail'
spring:
mail:
host: ${MAIL_HOST}
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
timeout: 50000
starttls.enable: true
debug: true
@GetMapping("/email")
public ResponseEntity<Map<String, String>> sendAuthNumber(@Valid @Pattern(regexp = "^[a-zA-Z0-9]+([._%+-]*[a-zA-Z0-9])*@([a-zA-Z0-9]+\\.)+[a-zA-Z]{2,}$",
message = "이메일을 입력해주세요") @RequestParam String email){
mailSendService.sendAuthNumber(email);
return ResponseEntity.status(201).body(setResponseMesssage("message", "인증번호를 보냈습니다."));
}
@PostMapping("/email")
public ResponseEntity<Map<String, String>> checkAuthNumber(@RequestParam String email, @RequestParam int authNumber){
mailSendService.checkAuthNumber(email, authNumber);
return ResponseEntity.status(200).body(setResponseMesssage("message", "이메일 인증을 성공했습니다."));
}
public interface MailSendService {
int sendAuthNumber(String email);
int makeTempNumber();
void sendEmailForAuth(String title, String email, String content);
void checkAuthNumber(String email, int authNumber);
void buildMail(int checkNumber,String email);
}
@Service
@RequiredArgsConstructor
public class MailSendServiceImpl implements MailSendService {
private final JavaMailSenderImpl javaMailSender;
private final VerificationCodeRepository verificationCodeRepository;
@Override
public int sendAuthNumber(String email) {
int authNumber = makeTempNumber();
buildMail(authNumber, email);
VerificationCode verificationCode = new VerificationCode(email, authNumber);
verificationCodeRepository.save(verificationCode);
return authNumber;
}
@Override
public void checkAuthNumber(String email, int authNumber) {
int expectedAuthNumber = verificationCodeRepository.findById(email).get().getAuthNumber();
boolean isNotMatchAuthCode = expectedAuthNumber != authNumber;
if (isNotMatchAuthCode) {
throw new ValidateException(HttpStatus.BAD_REQUEST, "인증 번호가 잘못되었습니다.");
}
}
@Override
public int makeTempNumber() {
Random random = new Random();
int checkNum = random.nextInt(888888) + 111111;
return checkNum;
}
@Override
public void buildMail(int checkNumber, String email) {
String title = "[올리] 메일 인증 코드 발송 ";
String content = "이메일 인증코드"+
"<br><br>" +
"인증번호는 " + checkNumber + " 입니다." +
"<br><br>" +
"해당 인증번호를 인증번호 확인란에 기입하여 주시기 바랍니다.";
sendEmailForAuth(title, email, content);
}
@Override
public void sendEmailForAuth(String title, String email, String content) {
try {
sendMail(title, email, content);
} catch (MessagingException e) {
throw new ServerException("인증번호 전송 실패");
}
}
private void sendMail(String title, String email, String content) throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
messageHelper.setTo(email);
messageHelper.setSubject(title);
messageHelper.setText(content, true);
javaMailSender.send(mimeMessage);
}
}
저는 이메일을 key 값으로 하여 인증번호를 저장하고 꺼내는 것만 할 것이기 때문에 RedisRepository를 선택하였고, 외부의 Redis 서버 없이도 Redis를 사용할 수 있는 Embedded Redis를 사용하여 구현하였습니다.
참고한 블로그입니다.
spring:
redis:
host: localhost
port: 6379
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
인증번호 데이터의 TimeToLive는 10분 동안 살아있도록 설정해두었습니다.
@Getter
@RedisHash(value = "VerificationCode", timeToLive = 600)
public class VerificationCode {
@Id
private String email;
private int authNumber;
public VerificationCode(String email, int authNumber) {
this.email = email;
this.authNumber = authNumber;
}
}
public interface VerificationCodeRepository extends CrudRepository<VerificationCode, String> {
}
테스트를 해보면 아래와 같이 잘 나온 것을 볼 수 있습니다. 💫
요즘 아래와 같이 html 파일로 인증번호를 보여주는 경우가 많은 것 같아서 후에 타임리프를 사용해서 인증번호를 보여주는 html을 만들어주면 더 좋을 것 같습니다.🙂