이메일 인증을 구현하려면 이메일 서버가 필요하다. 직접 이메일 서버를 만드는 것보다 네이버나 구글의 이메일 서버를 빌리는 것이 보안적인 측면, 구현적인 측면에서 좋을 것이라 판단하였고 이 글은 구글 이메일 서버를 빌려 진행할 것이다.
1) 구글 이메일에 접속한다. https://www.google.com/intl/ko/gmail/about/
2) 내 프로필을 클릭하고 Google 계정 관리에 들어간다.
3) 앱 비밀번호가 필요한데 이것을 만들려면 우리의 구글 계정에 2단계 인증이 필요하다. 보안탭에서 2단계 인증을 사용하는 로그인을 설정해준다.
4) 설정을 해준 뒤 앱 비밀번호를 생성해준다.
5) 생성 된 앱 비밀번호를 따로 저장해준다.(필수는 아님)

JavaMailApi를 직접 사용하기 위한 의존성
implementation 'javax.mail:mail:1.4.7'
// Spring Context Support
implementation 'org.springframework:spring-context-support:5.3.9'
스프링부트에서 제공하는 편리한 이메일 전송 기능을 위한 의존성으로, 스프링 생태계 내에서 통합되고 설정이 간단하게 처리
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '2.6.3'
build.gradle에 추가해준다.(2가지 방식 모두에서 작동되는 것을 확인 하였다.)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties;
@Configuration
public class EmailConfig {
@Bean
public JavaMailSender mailSender() { //JAVA MAILSENDER 인터페이스를 구현한 객체를 빈으로 등록하기 위함.
JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); //JavaMailSender 의 구현체를 생성하고
mailSender.setHost("smtp.gmail.com"); // 속성을 넣음, 이메일 전송에 사용할 SMTP 서버 호스트를 설정
mailSender.setPort(587);// 587로 포트를 지정
mailSender.setUsername("구글@gmail.com"); //구글계정
mailSender.setPassword("아까 저장한 앱비밀번호"); //구글 앱 비밀번호
Properties javaMailProperties = new Properties(); //JavaMail의 속성을 설정하기 위해 Properties 객체를 생성
javaMailProperties.put("mail.transport.protocol", "smtp"); //프로토콜로 smtp 사용
javaMailProperties.put("mail.smtp.auth", "true"); //smtp 서버에 인증이 필요
javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); //SSL 소켓 팩토리 클래스 사용
javaMailProperties.put("mail.smtp.starttls.enable", "true");//STARTTLS(TLS를 시작하는 명령)를 사용하여 암호화된 통신을 활성화
javaMailProperties.put("mail.debug", "true"); //디버깅 정보 출력
javaMailProperties.put("mail.smtp.ssl.trust", "smtp.gmail.com"); //smtp 서버의 ssl 인증서를 신뢰
javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2"); //사용할 ssl 프로토콜 버젼
mailSender.setJavaMailProperties(javaMailProperties);//mailSender에 우리가 만든 properties 넣고
return mailSender;//빈으로 등록
}
}
사용자의 이메일을 받아올 때 DTO로 받는다.
@Getter
@Setter
public class EmailRequestDTO {
@Email
//1)@기호를 포함해야 한다.
//2)@기호를 기준으로 이메일 주소를 이루는 로컬 호스트와 도메인 파트가 존재해야 한다.
//3)도메인 파트는 최소 하나의 점과 그 뒤에 최소한 2개의 알파벳을 가진다는 것을 검증
@NotEmpty(message = "이메일을 입력해 주세요.")
private String email;
}
@RestController
@RequiredArgsConstructor
public class MailController {
private final MailSendService mailService;
@PostMapping ("/mailSend")
public String mailSend(@RequestBody @Valid EmailRequestDTO emailDTO){
System.out.println("이메일 인증 요청이 들어옴");
System.out.println("이메일 인증 이메일 :" + emailDTO.getEmail());
return mailService.joinEmail(emailDTO.getEmail());
}
인증 번호를 생성하고 이메일을 보내는 서비스를 수행한다.
@Service
public class MailSendService {
@Autowired
private JavaMailSender mailSender;
private int authNumber;
//임의의 6자리 양수를 반환
public void makeRandomNumber() {
Random r = new Random();
String randomNumber = "";
for(int i = 0; i < 6; i++) {
randomNumber += Integer.toString(r.nextInt(10));
}
authNumber = Integer.parseInt(randomNumber);
}
//mail을 어디서 보내는지, 어디로 보내는지 , 인증 번호를 html 형식으로 어떻게 보내는지 작성
public String joinEmail(String email) {
makeRandomNumber();
String setFrom = "wlsrl515@gmail.com"; // email-config에 설정한 자신의 이메일 주소를 입력
String toMail = email;
String title = "회원 가입 인증 이메일 입니다."; // 이메일 제목
String content =
"Traveler를 방문해주셔서 감사합니다." + //html 형식으로 작성 !
"<br><br>" +
"인증 번호는 " + authNumber + "입니다." +
"<br>" +
"인증번호를 제대로 입력해주세요"; //이메일 내용 삽입
mailSend(setFrom, toMail, title, content);
return Integer.toString(authNumber);
}
//이메일 전송
public void mailSend(String setFrom, String toMail, String title, String content) {
MimeMessage message = mailSender.createMimeMessage();//JavaMailSender 객체를 사용하여 MimeMessage 객체를 생성
try {
MimeMessageHelper helper = new MimeMessageHelper(message,true,"utf-8");//이메일 메시지와 관련된 설정을 수행합니다.
// true를 전달하여 multipart 형식의 메시지를 지원하고, "utf-8"을 전달하여 문자 인코딩을 설정
helper.setFrom(setFrom);//이메일의 발신자 주소 설정
helper.setTo(toMail);//이메일의 수신자 주소 설정
helper.setSubject(title);//이메일의 제목을 설정
helper.setText(content,true);//이메일의 내용 설정 두 번째 매개 변수에 true를 설정하여 html 설정
mailSender.send(message);
} catch (MessagingException e) {//이메일 서버에 연결할 수 없거나, 잘못된 이메일 주소를 사용하거나, 인증 오류가 발생하는 등 오류
// 이러한 경우 MessagingException이 발생
e.printStackTrace();//e.printStackTrace()는 예외를 기본 오류 스트림에 출력하는 메서드
}
}
}
이로써 사용자에게 메일을 보내긴 했다. 또한 인증번호를 프론트엔드 쪽에다 JSON 형식으로 보내는 것을 완료했다. 이제 사용자가 인증번호를 입력하고 올바르게 입력 하였는지 확인하고 싶다. 그렇다면 사용자의 원래 인증번호를 DB에다가 저장해야 할까?? 단 몇 분만 지속되는 인증번호를 위해 DB에다가 저장하기엔 효율적이지 못하다고 생각한다. 그렇기 때문에 인메모리 DB인 Redis를 사용하여 사용자의 인증번호를 짧게 저장한다.
https://github.com/microsoftarchive/redis/releases 이 곳에서 다운로드를 진행한다.
build.gradle 에 추가한다.
//redis 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
# Redis configuration
redis:
host: localhost
port: 6379
사용자가 인증번호를 확인하고 인증번호를 입력하였을 때 받아오는 DTO 이다.
@Data
public class EmailCheckDTO {
@Email
@NotEmpty(message = "이메일을 입력해 주세요")
private String email;
@NotEmpty(message = "인증 번호를 입력해 주세요")
private String authNum;
}
@RestController
@RequiredArgsConstructor
public class MailController {
private final MailSendService mailService;
@PostMapping ("/mailSend")
public String mailSend(@RequestBody @Valid EmailRequestDTO emailDTO){
System.out.println("이메일 인증 요청이 들어옴");
System.out.println("이메일 인증 이메일 :" + emailDTO.getEmail());
return mailService.joinEmail(emailDTO.getEmail());
}
@PostMapping("/mailauthCheck")
public String AuthCheck(@RequestBody @Valid EmailCheckDTO emailCheckDTO){
Boolean Checked=mailService.CheckAuthNum(emailCheckDTO.getEmail(), emailCheckDTO.getAuthNum());
if(Checked){
return "ok";
}
else{
throw new NullPointerException("다시 시도해 주세요!");
}
}
}
예외가 터졌을 때 JSON으로 그 내용을 확인한다.
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
NullPointer 예외가 발생했을 때 이렇게 응답한다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
// NullPointer 예외가 발생했을 때 응답 방식
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<ErrorResult> testing(NullPointerException e) {
ErrorResult errorResult = new ErrorResult("EMAIL", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
}
@Service
@RequiredArgsConstructor
public class MailSendService {
private final JavaMailSender mailSender;
private final RedisUtil redisUtil;
private int authNumber; // 인증 번호
//추가
public boolean CheckAuthNum(String email, String authNum) {
if (redisUtil.getData(authNum) == null) {
return false;
} else if (redisUtil.getData(authNum).equals(email)) {
return true;
} else {
return false;
}
}
//임의의 6자리 양수를 반환
public void makeRandomNumber() {
Random r = new Random();
String randomNumber = "";
for (int i = 0; i < 6; i++) {
randomNumber += Integer.toString(r.nextInt(10));
}
authNumber = Integer.parseInt(randomNumber);
}
//mail을 어디서 보내는지, 어디로 보내는지 , 인증 번호를 html 형식으로 어떻게 보내는지 작성
public String joinEmail(String email) {
makeRandomNumber();
String setFrom = "wlsrl515@gmail.com"; // email-config에 설정한 자신의 이메일 주소를 입력
String toMail = email;
String title = "회원 가입 인증 이메일 입니다."; // 이메일 제목
String content =
"Traveler를 방문해주셔서 감사합니다." + //html 형식으로 작성 !
"<br><br>" +
"인증 번호는 " + authNumber + "입니다." +
"<br>" +
"인증번호를 제대로 입력해주세요"; //이메일 내용 삽입
mailSend(setFrom, toMail, title, content);
return Integer.toString(authNumber);
}
//이메일을 전송합니다.
public void mailSend(String setFrom, String toMail, String title, String content) {
MimeMessage message = mailSender.createMimeMessage();//JavaMailSender 객체를 사용하여 MimeMessage 객체를 생성
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true, "utf-8");//이메일 메시지와 관련된 설정을 수행합니다.
// true를 전달하여 multipart 형식의 메시지를 지원하고, "utf-8"을 전달하여 문자 인코딩을 설정
helper.setFrom(setFrom);//이메일의 발신자 주소 설정
helper.setTo(toMail);//이메일의 수신자 주소 설정
helper.setSubject(title);//이메일의 제목을 설정
helper.setText(content, true);//이메일의 내용 설정 두 번째 매개 변수에 true를 설정하여 html 설정
mailSender.send(message);
} catch (MessagingException e) {//이메일 서버에 연결할 수 없거나, 잘못된 이메일 주소를 사용하거나, 인증 오류가 발생하는 등 오류
// 이러한 경우 MessagingException이 발생
e.printStackTrace();//e.printStackTrace()는 예외를 기본 오류 스트림에 출력하는 메서드
}
redisUtil.setDataExpire(Integer.toString(authNumber), toMail, 60 * 5L); // 5분 동안 인증번호 유효
}
}
@Service
@RequiredArgsConstructor
public class RedisUtil {
private final StringRedisTemplate redisTemplate;//Redis에 접근하기 위한 Spring의 Redis 템플릿 클래스
public String getData(String key){//지정된 키(key)에 해당하는 데이터를 Redis에서 가져오는 메서드
ValueOperations<String,String> valueOperations=redisTemplate.opsForValue();
return valueOperations.get(key);
}
public void setData(String key,String value){//지정된 키(key)에 값을 저장하는 메서드
ValueOperations<String,String> valueOperations=redisTemplate.opsForValue();
valueOperations.set(key,value);
}
public void setDataExpire(String key,String value,long duration){//지정된 키(key)에 값을 저장하고, 지정된 시간(duration) 후에 데이터가 만료되도록 설정하는 메서드
ValueOperations<String,String> valueOperations=redisTemplate.opsForValue();
Duration expireDuration= Duration.ofSeconds(duration);
valueOperations.set(key,value,expireDuration);
}
public void deleteData(String key){//지정된 키(key)에 해당하는 데이터를 Redis에서 삭제하는 메서드
redisTemplate.delete(key);
}
}

인증번호가 잘 나온다는 것이 확인 가능하다.

구글 이메일함에도 인증번호가 도착한 것을 확인 가능하다.

인증번호를 입력하면 인증이 성공한 것을 확인 할 수 있다.