회원가입 후 스프링 시큐리티의 기능을 활용해 계정을 비활성화하고 이메일 인증을 하여 회원을 활성화하는 방식으로 가장 간단하게 이메일 인증을 구현하였다.
1. mail 의존성 추가.
implementation 'org.springframework.boot:spring-boot-starter-mail'
2. 구글 계정 생성 -> 2차 비밀번호 적용 -> 앱 비밀번호 생성.
이메일을 막 보내게되면 계정이 정지될 수도 있으니 새로운 계정으로 하는 걸 추천한다. 계정 생성 후 2차 비밀번호를 적용하면 앱 비밀번호를 생성할 수 있는데 이 앱 비밀번호를 이용해서 메일을 전송할 수 있다.
3. application.yml 설정
Gmail SMTP 연결을 위한 설정이다. 위에서 생성한 앱 비밀번호를 password칸에 넣어야 된다. email.send를 굳이 따로 둔 이유는 역할을 구분하기 위해서 이다. spring.mail.username 은 smtp 연결을 위한 정보이고, email.send는 메일 보내는 계정에 대한 정보이다.
spring:
mail:
host: smtp.gmail.com
port: 587
username: 구글 아이디
password: 앱 비밀번호
properties:
mail:
smtp:
auth: true
starttls:
enable: true
email.send: 구글 아이디
4. member 테이블에 enabled 필드 추가 (true면 계정 활성, false면 계정 비활성)
enabled 필드를 넣어서 이 값으로 계정 활성화 비활성화 여부를 설정한다.
ALTER TABLE member ADD COLUMN enabled boolean not null default false;
이어서 entity에 enabled 필드를 추가하고 UserDetails를 상속받아 구현한 isEnabled()에 enabled 값으로 활성화 비활성화 여부를 결정한다.
5. 메일을 보낼 template html를 만든다.
경로 : resources > template > mailTemplate.html
<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>이메일 인증 링크</title>
</head>
<body>
<span>계정 활성화를 위해서 아래의 인증 링크를 클릭해주세요.</span><br>
<a th:href="${link}">인증 링크</a>
</body>
</html>
6. template engine을 이용하여 link 값을 매핑해준다.
Template Engine은 템플릿 양식과 데이터를 합쳐서 HTML을 보여준다. context.setVariable("link", message) 를 통해서 위에서 만든 ${link} 에 대한 값을 매핑해 줄 수 있다.
package org.store.clothstar.common.mail;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
@Service
@RequiredArgsConstructor
public class MailContentBuilder {
private final TemplateEngine templateEngine;
public String build(String message) {
Context context = new Context();
context.setVariable("link", message);
return templateEngine.process("mailTemplate", context);
}
}
7. 메일 전송 Service를 만든다.
package org.store.clothstar.common.mail;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender mailSender;
@Value("${email.send}")
private String fromAddress;
public boolean sendMail(MailSendDTO mailSendDTO) {
MimeMessagePreparator messagePreparator = mimeMessage -> {
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
messageHelper.setFrom(fromAddress);
messageHelper.setTo(mailSendDTO.getAddress());
messageHelper.setSubject(mailSendDTO.getSubject());
messageHelper.setText(mailSendDTO.getText(), true);
};
log.info("전송 메일주소 : {} -> {}", fromAddress, mailSendDTO.getAddress());
mailSender.send(messagePreparator);
return true;
}
}
JavaMailSender는 MailService의 하위 인터페이스다. MailService는 SimpleMailMessage를 이용하여 보낸 사람, 받는 사람, 참조, 제목 및 텍스트 필드를 포함하는 간단한 메일 메시지를 만드는 데 사용되는데 JavaMailSender는 MimeMessage를 이용하여 메일을 보낼 수 있다.
참고 : MimeMessage는 SimpleMessage 와는 다르게 Multipart 기능이 있어서 첨부파일과 이미지를 보낼수있다.
MimeMessagePreparator는 MimeMessage 객체를 만들 수 있는 함수형 인터페이스이고 메일 메시지를 준비를 람다식으로 만들어서 코드의 가독성을 올릴수있다.
MimeMessageHelper는 MimeMessage를 편하게 만들 수 있는 도우미 역할을 한다. setFrom() 함수로 보내는 메일주소, setTo() 함수로 받는 메일주소, setSubject() 함수로 메일제목, setText()로 메일 내용값을 넣어서 MimeMessage를 넣는다.
메일 내용이 html 양식이기 때문에 setText()함수의 두번째 파라미터를 true로 한다.
메일 양식을 다 만들면 JavaMailSender의 send() 함수로 메일을 보낼수 있다.
[번외] 이메일 전송 테스트 코드 작성
메일 전송을 위한 테스트 코드이다. MailSendDTO 객체에 실제 메일을 보낼 이메일 주소를 넣고 테스트 코드를 실행하면 메일이 전송되는지 확인한다.
@SpringBootTest
class MailServiceTest {
@Autowired
MailService mailService;
@Autowired
MailContentBuilder mailContentBuilder;
@DisplayName("메일 전송 테스트")
@Test
void mailSendTest() {
//given
String link = "https://www.naver.com/";
String message = mailContentBuilder.build(link);
MailSendDTO mailSendDTO = new MailSendDTO("@naver.com", "test", message);
//when
Boolean success = mailService.sendMail(mailSendDTO);
//then
Assertions.assertThat(success).isTrue();
}
}
8. GlobalException 설정
JavaMailSender의 send() 함수로 메일을 보내다 에러가 생기면 RuntimeException인 MailException이 발생한다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
private ResponseEntity<ErrorResponseDTO> mailException(MailException ex) {
log.error("[MailException Handler] {}", ex.getMessage());
ex.fillInStackTrace();
ErrorResponseDTO errorResponseDTO = new ErrorResponseDTO(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
return new ResponseEntity<>(errorResponseDTO, HttpStatus.INTERNAL_SERVER_ERROR);
}
9. 회원가입 Service에서 이메일 전송 한다.
회원가입시 memberEntity의 필드 enabeld의 값을 false로 세팅하고 회원을 db에 저장한 다음 메일을 전송한다.
private void sendEmailAuthentication(Long memberId, String email) {
String link = "http://localhost:8080/v1/members/auth/" + memberId;
String message = mailContentBuilder.build(link);
MailSendDTO mailSendDTO = new MailSendDTO(email, "clothstar 회원가입 인증 메일 입니다.", message);
mailService.sendMail(mailSendDTO);
}
link주소는 위에서 생성한 mailTemplate.html의 ${link}로 매핑이 되고 mailContentBuilder.build(link)로 Template Engine으로 만들어진 메일 내용을 만들고 그러고 나서 MailSendDTO에 받는 사람의 mail 주소, 메일제목, 메일 내용을 담아 sendMail() 함수로 메일을 전송한다.
위 link를 클릭하면 단순히 member의 Enabled 필드가 true로 변경된다.
위 구현로직에서 문제점으로 회원가입을 시도하였지만 영원히 회원인증을 하지 않으면 DB에 데이터가 그대로 남아있다는 문제이다. 나중에 회원이 똑같은 아이디로 회원가입을 하지도 못하고 쓰레기 데이터가 계속 남아있다는 문제가 생긴다.
그래서 애초에 인증을 해야지만 DB에 데이터가 들어가도록 이메일에 인증번호를 보내고 그 인증번호를 입력하면 DB에 회원정보가 인입될 수 있도록 수정해야 한다. 인증번호를 확인하는 과정에서 DB를 사용하지 않고 Redis를 이용하려고 한다.
참고
- https://www.youtube.com/watch?v=w3bOn72icoU&list=PLbq5jHjpmq7q3cggEGN8TXnY-P2UbIA4p&index=11
- https://sdy-study.tistory.com/269
- https://www.baeldung.com/spring-email
'Project > B2C-Side-Project(second)' 카테고리의 다른 글
[SpringBoot] Redis 활용한 회원가입 이메일 인증 방법(개선후) (0) | 2024.07.06 |
---|---|
[SpringBoot] Github Actions 이용한 AWS EC2(ubuntu)에 자동배포와 디스코드 알림 설정 서버다운 해결 (0) | 2024.06.11 |
[SpringBoot] GlobalException 활용한 파라미터 유효성 검증 리팩토링 (0) | 2024.06.10 |
[프로젝트] 스프링시큐리티 5->6, JWT 버전 업그레이드 하고 error와 warning 수정 (0) | 2024.06.10 |
[프로젝트] DIP 지켜가며 기존 MyBatis기능에서 JPA기능으로 변경하기 (0) | 2024.06.09 |