Redis는 key-value
형태로 데이터를 저장하고 관리하는 데이터베이스로 TTL 을 설정할 수 있어 이메일 인증을 구현하는데 적합하다.
$ brew install redis
$ brew services start redis
$ brew services stop redis
$ brew services restart redis
$ redis-cli
implementation 'org.springframework.data:spring-data-redis'
implementation 'io.lettuce:lettuce-core:6.2.3.RELEASE'
spring:
data:
redis:
host: localhost
port: 6379
SMTP(Simple Mail Transfer Protocol
)는 인터넷을 통해 이메일 메시지를 보내고 받는데 사용되는 통신 프로토콜이고 SMTP 서버는 SMTP
를 사용하여 이메일을 전송하는 애플리케이션이다. Gmail
을 사용하여 이메일을 보내도록 하기위해 Gmail SMTP 서버를 사용하자.
Gmail 우측상단 톱니바퀴 -> 모든 설정 보기
전달 및 POP/IMAP -> IMAP 사용 -> 변경사항 저장
구글 계정 -> 보안 -> 2단계 인증
2단계 인증 -> 앱 비밀번호
이름 입력(ex. GMAIL_SMTP) -> 만들기
생성된 앱 비밀번호 확인 -> 앱 비밀번호 복사해서 다른 곳에 저장
GMAIL_SMTP 앱 생성된 것을 확인
implementation 'org.springframework.boot:spring-boot-starter-mail'
spring:
mail:
host: smtp.gmail.com
port: 587
username: [이메일]
password: [앱 비밀번호]
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
mail.smtp.starttls.required: true
mail.mime.charset: UTF-8
Gmail SMTP
는 포트 번호로 465
또는 587
사용가능하다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="inner">
<h2>아이디 찾기</h2>
<p>계정에 등록된 이메일을 인증하시면 <br/>사용중인 계정의 아이디를 알려드립니다.</p>
<form action="./showId.html" th:action th:object="${form}" th:method="post" th:onsubmit="return validateAuthCode()">
<div class="category email-category">
<label for="email">이메일<span style="color: #dc3545">*</span></label>
<div class="category-container">
<input type="text" id="email" th:field="*{email}" th:errorclass="field-error"/>
<button type="button" id="auth-request">인증요청</button>
</div>
<div class="field-error" th:errors="*{email}"></div>
</div>
<h4 class="auth-request-message"></h4>
<div class="category auth-category">
<label for="auth-code"></label>
<div class="category-container">
<input type="text" id="auth-code" th:field="*{authCode}" th:errorclass="field-error"
placeholder="인증번호 입력"/>
<button type="submit" id="auth-confirm">인증</button>
</div>
<div class="field-error" th:errors="*{authCode}"></div>
</div>
<h4 class="auth-confirm-message"></h4>
</form>
</div>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="../../static/js/findId.js" th:src="@{/js/findId.js}"></script>
</html>
RedisTemplate을 이용해 Redis
에 접근
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
LettuceConnectionFactory
: Lettuce
로 Redis
와 연결setKeySerializer
, setValueSerializer
: 직렬화, 역직렬화를 위해 설정@Service
@RequiredArgsConstructor
public class RedisUtil {
private final StringRedisTemplate redisTemplate;
public String getData(String key) {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
return valueOperations.get(key);
}
public boolean existData(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void setDataExpire(String key, String value, long duration) {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
Duration expireDuration = Duration.ofSeconds(duration);
valueOperations.set(key, value, expireDuration);
}
public void deleteData(String key) {
redisTemplate.delete(key);
}
}
getData()
: key
해당하는 value
조회existData()
: key
해당하는 value
존재유무 확인setDataExpire()
: 데이터에 만료 시간을 설정해서 key
와 value
를 Redis
에 저장deleteData()
: key
에 해당하는 데이터 삭제두가지 기능을 나누어서 설명하겠다.
1. 인증코드를 담은 메일 전송
2. 인증코드가 맞는지 확인
인증요청
버튼을 클릭Ajax
를 통해 [사용자가 입력한 이메일]
을 전달하면서 메일 전송을 스프링에 요청html
기반으로 메일을 작성하고 Gmail SMTP 서버
에게 [사용자가 입력한 이메일]로 메일 전송을 요청Gmail SMTP
가 메일을 전송하고 스프링은 Redis
에 [사용자가 입력한 이메일]
- 인증코드
를 저장한다. document.getElementById('auth-request').addEventListener('click', function () {
if(!hasEmailValue()) {
// 검증 코드
}
else if(!checkEmailRegex()) {
// 검증 코드
}
else {
$.ajax({
type: 'get',
url: '/email/auth?address=' + $('#email').val(),
async: false,
dataType: 'json',
success: function (result) {
let isSuccess = result['success'];
let message = result['message'];
if(isSuccess) {
// css 설정
} else {
// css 설정
}
}
});
}
});
GET
/email/auth?address=[사용자가 입력한 이메일]
dataType: 'json'
: 서버로부터 json
타입 반환받을 것이다. @RestController
@RequiredArgsConstructor
@RequestMapping("/email")
public class EmailController {
private final EmailService emailService;
@GetMapping("/auth")
public EmailAuthResponse sendAuthCode(@RequestParam String address) {
return emailService.sendEmail(address);
}
...
}
sendAuthCode()
호출 @Service
@RequiredArgsConstructor
public class EmailService {
@Value("${spring.mail.username}")
private String senderEmail;
private final JavaMailSender mailSender;
private final RedisUtil redisUtil;
public EmailAuthResponse sendEmail(String toEmail) {
if(redisUtil.existData(toEmail)) {
redisUtil.deleteData(toEmail);
}
try {
MimeMessage emailForm = createEmailForm(toEmail);
mailSender.send(emailForm);
return new EmailAuthResponse(true, "인증번호가 메일로 전송되었습니다.");
} catch(MessagingException | MailSendException e) {
return new EmailAuthResponse(false, "메일 전송 중 오류가 발생하였습니다. 다시 시도해주세요.");
}
}
private MimeMessage createEmailForm(String email) throws MessagingException {
String authCode = String.valueOf(ThreadLocalRandom.current().nextInt(100000, 1000000));
MimeMessage message = mailSender.createMimeMessage();
message.setFrom(senderEmail);
message.setRecipients(MimeMessage.RecipientType.TO, email);
message.setSubject("인증코드입니다.");
message.setText(setContext(authCode), "utf-8", "html");
redisUtil.setDataExpire(email, authCode, 10 * 60L); // 10분
return message;
}
private String setContext(String authCode) {
String body = "";
body += "<h4>" + "인증 코드를 입력하세요." + "</h4>";
body += "<h2>" + "[" + authCode + "]" + "</h2>";
return body;
}
...
}
sendEmail()
: 메일을 전송하는 메서드로 메일 전송 성공 여부와 사용자에게 보여줄 메세지를 담은 EmailAuthResponse
를 반환한다.createEmailForm()
: 메일 발신자, 메일 수신자, 메일 제목, 메일 내용을 설정하고 Redis
에 메일 수신자
와 인증코드
를 저장한다.setContext()
: 메일 내용 html
로 작성사용자가 메일로 받은 인증코드를 입력하고 인증
버튼을 클릭하면 submit
하기 전에 th:onsubmit="return validateAuthCode()"
에서 ajax
를 통해 올바른 인증코드인지 확인한다. 참고로 onsubmit는 form
전송을 하기 전에 입력된 데이터의 유효성을 체크하기 위해 사용하는 이벤트이다.
function validateAuthCode() {
let isAuthCodeValidate = false;
if(!hasAuthCodeValue()) {
// 검증 코드
..
} else {
$.ajax({
type: 'post',
url: '/email/auth?address=' + $('#email').val(),
async: false,
dataType: 'json',
data: {"authCode": $('#auth-code').val()},
success: function (result) {
let isSuccess = result['success'];
let message = result['message'];
if (isSuccess) {
isAuthCodeValidate = true;
} else {
// css 설정
}
}
})
}
return isAuthCodeValidate;
}
POST
/email/auth?address=[사용자가 입력한 이메일]
{"authCode : "[사용자가 입력한 인증코드]"}
dataType: 'json'
: 서버로부터 json
타입 반환받을 것이다. @RestController
@RequiredArgsConstructor
@RequestMapping("/email")
public class EmailController {
private final EmailService emailService;
...
@PostMapping("/auth")
public EmailAuthResponse checkAuthCode(@RequestParam String address, @RequestParam String authCode) {
return emailService.validateAuthCode(address, authCode);
}
}
checkAuthCode()
호출 @Service
@RequiredArgsConstructor
public class EmailService {
@Value("${spring.mail.username}")
private String senderEmail;
private final JavaMailSender mailSender;
private final RedisUtil redisUtil;
...
public EmailAuthResponse validateAuthCode(String email, String authCode) {
String findAuthCode = redisUtil.getData(email);
if(findAuthCode == null) {
return new EmailAuthResponse(false, "인증번호가 만료되었습니다. 다시 시도해주세요.");
}
if(findAuthCode.equals(authCode)) {
return new EmailAuthResponse(true, "인증 성공에 성공했습니다.");
} else {
return new EmailAuthResponse(false, "인증번호가 일치하지 않습니다.");
}
}
}
true
가 반환되어 submit
된다.@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
@GetMapping("/find-id")
public String findId(@ModelAttribute("form") FindIdForm form) {
return "members/findId";
}
@PostMapping("/find-id")
public String showId(@Valid @ModelAttribute("form") FindIdForm form, BindingResult bindingResult, Model model) {
if(bindingResult.hasErrors()) {
return "members/findId";
}
String encodedId = memberService.findLoginIdByEmail(form.getEmail());
model.addAttribute("encodedId", encodedId);
return "members/showId";
}
}
showId()
호출 : 이메일로 아이디 찾아서 반환한다. Redis
에 잘 저장된 것을 확인
오 도움되는 글 감사하무니다