계속해서 쇼핑몰 관련 기능들을 추가하고 있다. 이번에 또 어떤 기능을 추가할까 고민하던 차에 회원가입 할 때, 사용할 수 있는 기능을 구현해보고자 하였다. 바로 이메일 인증 기능이다.
사실 이메일 인증 기능은 사용자보다는 관리자입장에서 더 중요한 기능일 것이다. 만약 쇼핑몰 측에서 어떤 사용자가 경품이 당첨되어 상품을 배송하여야 하는 상황이라고 가정해보자. 그리고 쇼핑몰은 오직 이메일이라는 채널을 통해서만 이를 고지할 수 밖에 없다.
이 때, 사용자가 회원가입시 인증 절차를 거치지 않는다는 이점을 활용하여 그냥 아무렇게 없는 계정(ex. aaa@naver.com) 을 입력하여 회원가입을 했다면 어떻게 될까? 당연하게도 사용자에게 알릴 방법이 전혀 없게 될 것이다.😅
이외에도 블랙리스트를 판별, 관리하기 쉽다는 점 등이 이메일 인증 기능의 장점이다.
이메일 인증 기능을 구현하기 위해 구글링을 많이 했는데, 자바썸 님의 블로그 (https://gwamssoju.tistory.com/108) 에서 도움을 가장 많이 받았다. 감사합니다ㅎㅎ (꾸벅)
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
먼저, spring에 mail 관련 라이브러리를 추가하기 위해 의존성을 주입해준다. 나의 경우, maven으로 프로젝트를 구성했기 때문에 pom.xml
에 의존성을 주입해주었다.
의존성을 추가한다고 해서 바로 사용할 수 있는 것이 아니다. smtp 설정을 해주어야 스프링에서 메일 기능을 사용할 수 있게 된다. SMTP란 google, naver 등에서 메일 전송을 위한 프로토콜이다. 이 것을 스프링에 탑재해주는 것이다. application.properties
에 다음을 작성해준다.
application.properties
spring.mail.host=smtp.gmail.com
spring.mail.port=465
spring.mail.username=[본인이 사용할 이메일]
spring.mail.password=[생성된 비밀번호]
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.starttls.enable=true
username
에는 본인의 이메일을 넣으면 되지만 password
는 이메일의 비밀번호가 아니라 발급된 다른 비밀번호가 필요하다. 나는 이에 대해 잘 정리해놓은 블로그(https://hyunmin1906.tistory.com/276)를 통해 설정을 잘 마칠 수 있었다.이메일을 보내기 위해 Controller를 먼저 만들자.
MailController.java
package com.shop.controller;
import com.shop.service.MailService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequiredArgsConstructor
public class MailController {
private final MailService mailService;
@ResponseBody
@PostMapping("/mail")
public String MailSend(String mail){
int number = mailService.sendMail(mail);
String num = "" + number;
return num;
}
}
/mail
이라는 URL에 접속하게 되면 Post 방식으로 mail을 전송하도록 로직을 구성했다.@ResponseBody
어노테이션을 붙여 Mail Request 에 대한 Response 메소드를 만들었다.MailService.java
package com.shop.service;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
@Service
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender javaMailSender;
private static final String senderEmail= "wisejohn950330@gmail.com";
private static int number;
public static void createNumber(){
number = (int)(Math.random() * (90000)) + 100000;// (int) Math.random() * (최댓값-최소값+1) + 최소값
}
public MimeMessage CreateMail(String mail){
createNumber();
MimeMessage message = javaMailSender.createMimeMessage();
try {
message.setFrom(senderEmail);
message.setRecipients(MimeMessage.RecipientType.TO, mail);
message.setSubject("이메일 인증");
String body = "";
body += "<h3>" + "요청하신 인증 번호입니다." + "</h3>";
body += "<h1>" + number + "</h1>";
body += "<h3>" + "감사합니다." + "</h3>";
message.setText(body,"UTF-8", "html");
} catch (MessagingException e) {
e.printStackTrace();
}
return message;
}
public int sendMail(String mail){
MimeMessage message = CreateMail(mail);
javaMailSender.send(message);
return number;
}
}
View를 통해 Controller로 들어온 값을 가지고 Service 로직을 수행하면 원하는 내용의 이메일을 받을수 있게 된다. 기능적으로 구현만 하면 되기에 간단하게 메시지를 구성하였다.
createNumber
메소드는 Math.random 라이브러리를 이용하여 랜덤으로 난수를 생성하게 해준다.JavaMailSender
는 Java Mail API의 MimeMessage
를 이용해서 메일을 발송하는 추가적인 기능을 수행하게 해준다.memberForm.html
<!DOCTYPE html>
<!-- 생략 -->
<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:"/mail",
type:"post",
dataType:"json",
data:{"mail" : $("#mail").val()},
success: function(data){
alert("인증번호 발송");
$("#Confirm").attr("value",data);
},
}
});
}
function confirmNumber(){
var number1 = $("#number").val();
var number2 = $("#Confirm").val();
if(number1 == number2){
alert("인증되었습니다.");
}else{
alert("번호가 다릅니다.");
}
}
</script>
<div layout:fragment="content">
<form role="form" method="post" th:object="${memberFormDto}">
<div class="form-group">
<!-- 생략 -->
<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" style="width:250px; margin-top: -10px" placeholder="인증번호 입력">
<button type="button" name="confirmBtn" id="confirmBtn" onclick="confirmNumber()">이메일 인증</button>
<!-- 생략 -->
ajax
를 사용하였다.confirmNumber
함수를 통해 전송된 이메일 인증번호와 입력한 인증번호가 같다면 인증되었습니다를 출력, 아니라면 번호가 다르다는 메세지를 띄우게 만들었다.이렇게 모든 기능을 구현했다. 기능 실행만이 남아있다.
기대하는 마음으로 이메일 전송하기를 눌렀지만!!
안타깝게도 아무런 반응이 없었다...😥
한 차례 좌절을 겪었지만 개발을 하면서 늘 겪는 일상이기에..
트러블 슈팅을 위해 일단 어떤 에러를 뿜고 있는지 확인하기 위해 ajax에 다음과 같은 코드를 추가해 에러 로그가 담긴 창을 띄워보도록 해봤다.
,error:function(request, status, error){
alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error);
}
그랬더니 평소에 생소했던 상태코드 401 번의 에러가 나왔다. 어떤 오류인지 정확히 알지 못해 검색해보았더니 다음과 같은 에러였다.
즉, 한 마디로 "너 여기 들어올 자격 없으니까 못들어와" 였다. 무엇인가 이상했다. 회원가입은 인증 절차 없이 누구나 할 수 있으며 이에 따라 이메일 전송 또한 어떤 유저라도 상관없이 모두 동작해야만 한다. 다시 시작된 구글링....
처음에는 백 쪽 문제가 아니라 View단에서의 문제라고 생각했다. ajax에서 데이터를 담아 전송할 때, 뭔가에 막혀 전송이 안되고 있다고 여겨졌다. 찾아보니 앱의 인증 정보가 잘못되어서 그렇게 되는 경우가 있다고 했다. 그러던 중 Ajax로 POST 방식 요청 시 CORS 정책 이슈가 있다는 것을 알아냈다.
📌 CORS란?
Cross Origin Resource Sharing의 약자로 한 도메인 또는 Origin의 웹 페이지가 다른 도메인 (도메인 간 요청)을 가진 리소스에 액세스 할 수 있게하는 보안 메커니즘
즉, 보안상의 이유로 동일한 출처(도메인)에서만 리소스를 공유할 수 있다는 뜻이다. 이를 해결하기 위해 post 메세지를 날릴 때 header에 CORS를 허용할 수 있도록 하는 속성을 추가했다.
$.ajax({
url:"/mail",
type:"post",
crossDomain: true,
headers: { 'Access-Control-Allow-Origin': 'http://The web site allowed to access' },
dataType:"json",
그러나...결과는 FAIL.. 해결되지 않고 계속해서 401 에러를 마주하게 되었다.
문득 Spring Security에서 문제가 있는 것은 아닌가라는 생각이 들었다. 그도 그럴 것이 현재 프로젝트에서 인증과 인가를 담당하고 있는 곳은 Security 관련 설정인데 /mail
에 인가가 되지않아 들어가지 못하고 있다는 것은 시큐리티에서 무엇인가 설정을 해주면 될 수도 있지않을까라는 결론에 이르게 되었다.
열심히 구글링하던 중...! SecurityConfig 에서 http.authorizeRequests()
가 권한 설정을 담당한다는 것을 알아냈다. 무엇인가 빛✨이 보였다...
SecurityConfig.java
package com.shop.config;
// (... import 생략)
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig {
@Autowired
MemberService memberService;
@Autowired
private PrincipalOauth2UserService principalOauth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// http.formLogin() 부분 생략
http.authorizeRequests()
.mvcMatchers("/css/**", "/js/**", "/img/**").permitAll() // 모든 사용자가 인증(로그인) 없이 해당 경로에 접근할 수 있도록 설정
.mvcMatchers("/", "/members/**", "/item/**", "/images/**", "/mail/**").permitAll()
mvcMathcers
에서 .permitAll()
기능은 사용하면 관련 URL로 접속하면 별다른 인증 절차 없이 모든 사용자가 경로에 접근할 수 있는 기능이다.이 기능에 /mail/**
을 추가함으로써 /mail
로 접속하는 모든 사용자에게 권한을 해제하도록 했다. 이메일 전송버튼은 /mail
에 연결되어 실행하도록 로직을 짰기 때문에 정상대로 작동하게 된다면 전송버튼을 누른다면 이메일 전송이 정상적으로 되어야한다.
과연 결과는....?
(내 마음을 가장 잘 표현하는 짤)
과연 제가 이 문제를 해결했는지 궁금하시다면 다음 포스팅에서...😂
view form에 confirm을 아이디로 가지는 칸이 없는데 어떻게 코드가 실행되는건가요?