목차
회원가입 기능을 개발하면서 이메일로 회원가입할 수 있는 기능을 개발중이다. 이메일이 정상적으로 사용할 수 있는 지를 확인할 필요가 있다. 그래서 이메일로 인증코드가 발송되고, 인증코드를 인증하는 기능을 구현하려고 한다.
이메일 인증 구현 로직
개발 환경
IDE(개발툴) : IntelliJ
JDK(자바 버전) : 17
STS(스프링부트 버전) : 3.1.0
플러그인 추가 : build.gradle
구글 이메일 서비스(SMTP)
Redis 사용
구글 SMTP 서비스 설정하기
먼저 이메일을 보내기 위해서는 SMTP 서비스를 이용해야한다. SMTP란 인터넷을 통해 이메일을 주고 받을 때 사용하는 기능이다. 개인이 개발을 진행하면 SMTP 서버를 구현해서 서비스를 이용하기엔 복잡하다. 그래서 구글이나 네이버에서 SMTP 기능을 이용할 수 있게 제공하고 있다. 필자는 구글의 SMTP 서비스를 이용하려고 한다.
1. 먼저 구글 서비스로 로그인 후 Gmail 로 들어간다.
2. Gmail 화면에서 오른쪽 상단의 톱니바퀴 모양의 버튼을 클릭한다.
3. 제일 위에 빠른설정 - [ 모든 설정 보기 ] 메뉴를 클릭한다.
4. 이메일 설정 화면으로 이동된다.
5. 설정 제목 밑에 [ 전달 및 POP/IMAP ] 메뉴를 클릭한다.
6. 위 이미지에서 보여진느 것 처럼 설정 후 제일 아래에 있는 [ 변경사항 저장 ] 버튼을 클릭하여 설정한 정보를 저장한다.
구글 계정 비밀번호 설정하기
1. 구글 계정 옆의 바둑판 모양의 버튼을 클릭한다.
2. 위의 화면처럼 나타나면 [ 계정 ] 메뉴를 클릭한다.
3. 왼쪽 메뉴에서 [ 보안 ] 을 클릭 후 오른족에 나타난 보안 화면의 [ 2단계 인증 ] 을 클릭한다.
4. 제일 밑에 [ 앱 비밀번호 ] 클릭한다. 메뉴를 클릭한다.
5. 이동한 화면에서 앱 이름에 아무 문구나 입력 후 [ 만들기 ] 버튼을 클릭하면 앱 비밀번호가 생성되면서 팝업창에 표시된다. 이 앱 비밀번호는 스프링부트에서 smtp 설정을 위해 필요한 비밀번호 이기 때문에 따로 적어둬야 한다.
개발 코드
파일 구조
mail.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div style="margin:120px">
<div style="margin-bottom: 10px">
<h1>인증 코드 메일입니다.</h1>
<br/>
<h3 style="text-align: center;"> 아래 코드를 사이트에 입력해주십시오</h3>
</div>
<div style="text-align: center;">
<h2 style="color: crimson;" th:text="${code}"></h2>
</div>
<br/>
</div>
</body>
</html>
emailSend.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>인증코드 구현하기</title>
</head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript">
function sendNumber() {
$("#mail_number").css("display", "block");
$.ajax({
url: "/api/v1/email/send",
type: "post",
dataType: "json",
data: {"mail": $("#mail").val()},
success: function (data) {
alert("인증번호 발송");
$("#Confirm").attr("value", data);
}
});
}
function confirmNumber() {
let mail = $("#mail").val();
let number = $("#number").val();
$.ajax({
url: "/api/v1/email/verify",
type: "post",
dataType: "json",
data: {"mail": mail, "verifyCode": number},
success: function (data, status, xhr) {
console.log(status);
}
});
}
</script>
<body>
<div id="mail_input" name="mail_input">
<input type="text" name="mail" id="mail" placeholder="이메일 입력">
<button type="button" id="sendBtn" name="sendBtn" onclick="sendNumber()">인증번호</button>
</div>
<br>
<div id="mail_number" name="mail_number" style="display: none">
<input type="text" name="number" id="number" placeholder="인증번호 입력">
<button type="button" name="confirmBtn" id="confirmBtn" onclick="confirmNumber()">이메일 인증</button>
</div>
<br>
<input type="text" id="Confirm" name="Confirm" style="display: none" value="">
</body>
</html>
PageController
package com.study.email_verification.api.controller;
import com.study.email_verification.api.dto.EmailDto;
import com.study.email_verification.api.service.EmailService;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/email")
public class EmailController {
private final EmailService emailService;
// 인증코드 메일 발송
@PostMapping("/send")
public String mailSend(EmailDto emailDto) throws MessagingException {
log.info("EmailController.mailSend()");
emailService.sendEmail(emailDto.getMail());
return "인증코드가 발송되었습니다.";
}
// 인증코드 인증
@PostMapping("/verify")
public String verify(EmailDto emailDto) {
log.info("EmailController.verify()");
boolean isVerify = emailService.verifyEmailCode(emailDto.getMail(), emailDto.getVerifyCode());
return isVerify ? "인증이 완료되었습니다." : "인증 실패하셨습니다.";
}
}
EmailController
package com.study.email_verification.api.controller;
import com.study.email_verification.api.dto.EmailDto;
import com.study.email_verification.api.service.EmailService;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/email")
public class EmailController {
private final EmailService emailService;
// 인증코드 메일 발송
@PostMapping("/send")
public String mailSend(EmailDto emailDto) throws MessagingException {
log.info("EmailController.mailSend()");
emailService.sendEmail(emailDto.getMail());
return "인증코드가 발송되었습니다.";
}
// 인증코드 인증
@PostMapping("/verify")
public String verify(EmailDto emailDto) {
log.info("EmailController.verify()");
boolean isVerify = emailService.verifyEmailCode(emailDto.getMail(), emailDto.getVerifyCode());
return isVerify ? "인증이 완료되었습니다." : "인증 실패하셨습니다.";
}
}
EmailDto
package com.study.email_verification.api.dto;
import lombok.Data;
@Data
public class EmailDto {
// 이메일 주소
private String mail;
// 인증 코드
private String verifyCode;
}
EmailRepository
package com.study.email_verification.api.repository;
import com.study.email_verification.entity.Email;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface EmailRepository extends JpaRepository<Email, Long> {
// 인증코드 발송한 이메일 주소 조회
public Optional<Email> findByEmail(String email);
}
EmailService
package com.study.email_verification.api.service;
import com.study.email_verification.common.util.RedisUtil;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import java.util.Random;
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender javaMailSender;
private final RedisUtil redisUtil;
private static final String senderEmail = "sanbyul1@naver.com";
private String createCode() {
int leftLimit = 48; // number '0'
int rightLimit = 122; // alphabet 'z'
int targetStringLength = 6;
Random random = new Random();
return random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 | i >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
// 이메일 내용 초기화
private String setContext(String code) {
Context context = new Context();
TemplateEngine templateEngine = new TemplateEngine();
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
context.setVariable("code", code);
templateResolver.setPrefix("templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(false);
templateEngine.setTemplateResolver(templateResolver);
return templateEngine.process("mail", context);
}
// 이메일 폼 생성
private MimeMessage createEmailForm(String email) throws MessagingException {
String authCode = createCode();
MimeMessage message = javaMailSender.createMimeMessage();
message.addRecipients(MimeMessage.RecipientType.TO, email);
message.setSubject("안녕하세요. 인증번호입니다.");
message.setFrom(senderEmail);
message.setText(setContext(authCode), "utf-8", "html");
// Redis 에 해당 인증코드 인증 시간 설정
redisUtil.setDataExpire(email, authCode, 60 * 30L);
return message;
}
// 인증코드 이메일 발송
public void sendEmail(String toEmail) throws MessagingException {
if (redisUtil.existData(toEmail)) {
redisUtil.deleteData(toEmail);
}
// 이메일 폼 생성
MimeMessage emailForm = createEmailForm(toEmail);
// 이메일 발송
javaMailSender.send(emailForm);
}
// 코드 검증
public Boolean verifyEmailCode(String email, String code) {
String codeFoundByEmail = redisUtil.getData(email);
log.info("code found by email: " + codeFoundByEmail);
if (codeFoundByEmail == null) {
return false;
}
return codeFoundByEmail.equals(code);
}
}
EmailConfig
package com.study.email_verification.common.config;
import org.springframework.beans.factory.annotation.Value;
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 {
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.port}")
private int port;
@Value("${spring.mail.username}")
private String username;
@Value("${spring.mail.password}")
private String password;
@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;
@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private boolean starttlsEnable;
@Value("${spring.mail.properties.mail.smtp.starttls.required}")
private boolean starttlsRequired;
@Value("${spring.mail.properties.mail.smtp.connectiontimeout}")
private int connectionTimeout;
@Value("${spring.mail.properties.mail.smtp.timeout}")
private int timeout;
@Value("${spring.mail.properties.mail.smtp.writetimeout}")
private int writeTimeout;
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
mailSender.setDefaultEncoding("UTF-8");
mailSender.setJavaMailProperties(getMailProperties());
return mailSender;
}
private Properties getMailProperties() {
Properties properties = new Properties();
properties.put("mail.smtp.auth", auth);
properties.put("mail.smtp.starttls.enable", starttlsEnable);
properties.put("mail.smtp.starttls.required", starttlsRequired);
properties.put("mail.smtp.connectiontimeout", connectionTimeout);
properties.put("mail.smtp.timeout", timeout);
properties.put("mail.smtp.writetimeout", writeTimeout);
return properties;
}
}
RedisConfig
package com.study.email_verification.common.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
RedisUtil
package com.study.email_verification.common.util;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.time.Duration;
@RequiredArgsConstructor
@Service
public class RedisUtil {
private final StringRedisTemplate template;
public String getData(String key) {
ValueOperations<String, String> valueOperations = template.opsForValue();
return valueOperations.get(key);
}
public boolean existData(String key) {
return Boolean.TRUE.equals(template.hasKey(key));
}
public void setDataExpire(String key, String value, long duration) {
ValueOperations<String, String> valueOperations = template.opsForValue();
Duration expireDuration = Duration.ofSeconds(duration);
valueOperations.set(key, value, expireDuration);
}
public void deleteData(String key) {
template.delete(key);
}
}
package com.study.email_verification.entity;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@Entity
@Table(name = "email")
public class Email {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "email_id", unique = true, nullable = false)
private Long id;
// 이메일 주소
@Column(name = "email", nullable = false)
private String email;
// 이메일 인증 여부
@Column(name = "email_status", nullable = false)
private boolean emailStatus;
@Builder
public Email(String email) {
this.email = email;
this.emailStatus = false;
}
}
application.yaml
spring:
datasource:
hikari:
maximum-pool-size: 4
username: testuser
password: testpass
url: jdbc:postgresql://localhost:5432/study
# JPA
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
show-sql: true
# Email
mail:
host: smtp.gmail.com
port: 587
username: {google 이메일}
password: {발급받은 앱 비밀번호}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000
auth-code-expiration-millis: 1800000 # 30 * 60 * 1000 == 30?
# Redis
data:
redis:
host: localhost
port: 6379
'Framework > Springboot' 카테고리의 다른 글
PostgresSQL 사용시 엔티티의 Default 값 설정하기 (0) | 2024.02.09 |
---|---|
스프링부트로 파일 업로드 구현하기 (0) | 2024.02.07 |
스프링부트로 이메일 인증 서비스 구현하기 (0) | 2024.02.03 |
스프링부트로 이메일 인증 서비스 구현하기 (0) | 2024.01.31 |
스프링부트에서 html 템플릿 조회하기 (0) | 2024.01.30 |
댓글