회원가입 이메일 인증 기능 구현 -1-

jihan kong·2023년 6월 6일
3
post-thumbnail

계속해서 쇼핑몰 관련 기능들을 추가하고 있다. 이번에 또 어떤 기능을 추가할까 고민하던 차에 회원가입 할 때, 사용할 수 있는 기능을 구현해보고자 하였다. 바로 이메일 인증 기능이다.

사실 이메일 인증 기능은 사용자보다는 관리자입장에서 더 중요한 기능일 것이다. 만약 쇼핑몰 측에서 어떤 사용자가 경품이 당첨되어 상품을 배송하여야 하는 상황이라고 가정해보자. 그리고 쇼핑몰은 오직 이메일이라는 채널을 통해서만 이를 고지할 수 밖에 없다.

이 때, 사용자가 회원가입시 인증 절차를 거치지 않는다는 이점을 활용하여 그냥 아무렇게 없는 계정(ex. aaa@naver.com) 을 입력하여 회원가입을 했다면 어떻게 될까? 당연하게도 사용자에게 알릴 방법이 전혀 없게 될 것이다.😅
이외에도 블랙리스트를 판별, 관리하기 쉽다는 점 등이 이메일 인증 기능의 장점이다.

이메일 인증 기능을 구현하기 위해 구글링을 많이 했는데, 자바썸 님의 블로그 (https://gwamssoju.tistory.com/108) 에서 도움을 가장 많이 받았다. 감사합니다ㅎㅎ (꾸벅)

1. 의존성 추가

pom.xml

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

먼저, spring에 mail 관련 라이브러리를 추가하기 위해 의존성을 주입해준다. 나의 경우, maven으로 프로젝트를 구성했기 때문에 pom.xml 에 의존성을 주입해주었다.

2. Google SMTP

의존성을 추가한다고 해서 바로 사용할 수 있는 것이 아니다. 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
  • SMTP는 465 또는 587 포트 번호를 사용할 수 있다.
  • username 에는 본인의 이메일을 넣으면 되지만 password 는 이메일의 비밀번호가 아니라 발급된 다른 비밀번호가 필요하다. 나는 이에 대해 잘 정리해놓은 블로그(https://hyunmin1906.tistory.com/276)를 통해 설정을 잘 마칠 수 있었다.

3. MailController 생성

이메일을 보내기 위해 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 메소드를 만들었다.

4. MailService 생성

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 라이브러리를 이용하여 랜덤으로 난수를 생성하게 해준다.
  • MailSender 인터페이스를 상속받은 JavaMailSender 는 Java Mail API의 MimeMessage 를 이용해서 메일을 발송하는 추가적인 기능을 수행하게 해준다.

5. View

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 를 사용하였다.
  • post 방식으로 url을 연결하여 만약 데이터가 잘 전송된다면 (이메일이 전송되었다면) 인증번호가 발송되었다는 메세지를 띄우도록 했다.
  • confirmNumber 함수를 통해 전송된 이메일 인증번호와 입력한 인증번호가 같다면 인증되었습니다를 출력, 아니라면 번호가 다르다는 메세지를 띄우게 만들었다.

이렇게 모든 기능을 구현했다. 기능 실행만이 남아있다.

6. 오류 발생과 해결 과정 ⚠️

기대하는 마음으로 이메일 전송하기를 눌렀지만!!
안타깝게도 아무런 반응이 없었다...😥

한 차례 좌절을 겪었지만 개발을 하면서 늘 겪는 일상이기에..
트러블 슈팅을 위해 일단 어떤 에러를 뿜고 있는지 확인하기 위해 ajax에 다음과 같은 코드를 추가해 에러 로그가 담긴 창을 띄워보도록 해봤다.

,error:function(request, status, error){
		      alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error);
	        }

그랬더니 평소에 생소했던 상태코드 401 번의 에러가 나왔다. 어떤 오류인지 정확히 알지 못해 검색해보았더니 다음과 같은 에러였다.

즉, 한 마디로 "너 여기 들어올 자격 없으니까 못들어와" 였다. 무엇인가 이상했다. 회원가입은 인증 절차 없이 누구나 할 수 있으며 이에 따라 이메일 전송 또한 어떤 유저라도 상관없이 모두 동작해야만 한다. 다시 시작된 구글링....

1. Ajax 관련 문제?

처음에는 백 쪽 문제가 아니라 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 에러를 마주하게 되었다.


2. Spring Security?

문득 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 에 연결되어 실행하도록 로직을 짰기 때문에 정상대로 작동하게 된다면 전송버튼을 누른다면 이메일 전송이 정상적으로 되어야한다.


과연 결과는....?



(내 마음을 가장 잘 표현하는 짤)

과연 제가 이 문제를 해결했는지 궁금하시다면 다음 포스팅에서...😂

profile
학습하며 도전하는 것을 즐기는 개발자

3개의 댓글

comment-user-thumbnail
2023년 6월 13일

view form에 confirm을 아이디로 가지는 칸이 없는데 어떻게 코드가 실행되는건가요?

1개의 답글
comment-user-thumbnail
2023년 8월 22일

에러코드 500은 어떻게 해야할까요?

답글 달기