Spring 이메일 인증 - SMTP

맑은 눈의 코드 👀·2023년 8월 18일
0

06_framework 이론

목록 보기
12/23

📧이메일 인증 발급 순서

https://cloudtechflow.com/entry/%EA%B5%AC%EA%B8%80-%EC%95%B1-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0

  1. 구글 앱 비밀번호 발급 받기
  2. pom.xml에 라이브러리 추가
  3. email-context.xml 추가
  4. web.xml에 email-context.xml 경로 등록
  5. db에 이메일 인증키 테이블 생성
  6. VS Code signUp.jsp, signUp.js 에서 코드 작성
  7. EmailController.java, EmailService.java, EmailServiceImpl.java, EmailDAO.java, email-mapper.xml 생성
  8. mybatis-config.xml에 mapper 등록
  9. 아래 코드 작성

pom.xml

<dependencies>
  <!-- 이메일 인증 -->
		<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
		<dependency>
			<groupId>javax.mail</groupId>
			<artifactId>mail</artifactId>
			<version>1.4.7</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context-support</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
</dependencies>

email-context.xaml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- email 인증 관련 bean 생성  -->
    <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="host" value="smtp.gmail.com" />
        <property name="port" value="587" />
        <property name="username" value="이메일"/>
        <property name="password" value="앱비밀번호" />
        <property name="javaMailProperties">
            <props>
                <prop key="mail.transport.protocol">smtp</prop>
                <prop key="mail.smtp.auth">true</prop>
                <prop key="mail.smtp.starttls.enable">true</prop>
                <!-- <prop key="mail.debug">true</prop> -->
                <prop key="mail.smtp.ssl.trust">smtp.gmail.com</prop>
            <prop key="mail.smtp.ssl.protocols">TLSv1.2</prop>
            </props>
        </property>
    </bean>
</beans>

web.xml

<web-app>
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath:spring/root-context.xml
			classpath:spring/spring-security.xml
			classpath:spring/email-context.xml//******추가****
		</param-value>
	</context-param>


</web-app>

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" >
<configuration>
	<!-- SqlSessionTemplate 생성 시 적용될 설정 작성 부분 -->
	<settings>
		<!-- insert 또는 update에 사용되는 값 중 null이 있을 경우에 대한 설정 
			해당 설정이 없을 경우 -> SQL 
			구문에 null 포함되어 있다는 오류 발생.
			해당 설정이 있을 경우 -> 오류를 발생 시키지 않고 NULL 값을 컬럼에 대입
			단, NOT NULL 제약조건이 없는 컬럼에만 가능함.
			** value 설정 시 NULL 은 반드시 대문자로 작성 (소문자 null은 오류가 발생함)
		 -->
		<setting name="jdbcTypeForNull" value="NULL" />
	</settings>
	
	<!-- 별칭 작성 부분 -->
	<!-- VO클래스의 패키지명 + 클래스명 작성하는 것이 불편하기 때문에 짧은 별칭 부여 -->
	<typeAliases>
		<typeAlias type="edu.kh.project.member.model.dto.Member" alias="Member" />
	</typeAliases>
	
	<!-- mapper 파일(SQL 작성되는파일) 위치 등록 부분 -->
	<mappers>
		<mapper resource="/mappers/member-mapper.xml" />
		<mapper resource="/mappers/myPage-mapper.xml" />
		<mapper resource="/mappers/ajax-mapper.xml" />
		<mapper resource="/mappers/email-mapper.xml" />//*********추가********
	</mappers>
</configuration>

DB 테이블 생성

--이메일인증키 테이블
DROP TABLE "AUTH_KEY";

CREATE TABLE "AUTH_KEY" (
   "AUTH_KEY_NO"   NUMBER      NOT NULL,
   "CODE"   CHAR(6)      NOT NULL,
   "EMAIL"   VARCHAR2(50)      NOT NULL,
   "CREATE_TIME"   DATE   DEFAULT SYSDATE   NOT NULL
);



COMMENT ON COLUMN "AUTH_KEY"."AUTH_KEY_NO" IS '인증키 구분 번호(SEQ_AUTH_KEY_NO)';

COMMENT ON COLUMN "AUTH_KEY"."CODE" IS '코드';

COMMENT ON COLUMN "AUTH_KEY"."EMAIL" IS '이메일';

COMMENT ON COLUMN "AUTH_KEY"."CREATE_TIME" IS '인증 코드 생성 시간';

ALTER TABLE "AUTH_KEY" ADD CONSTRAINT "PK_AUTH_KEY" PRIMARY KEY (
   "AUTH_KEY_NO"
);

CREATE SEQUENCE SEQ_AUTH_KEY_NO NOCACHE;


UPDATE "AUTH_KEY" SET
CODE = #{authkey},
CREATE_TIME = sysdate
WHERE EMAIL = #{email};

INSERT INTO "AUTH_KEY" VALUES(SEQ_AUTH_KEY_NO.NEXTVAL, #{authkey}, #{email}, DEFAULT);

SELECT * FROM "AUTH_KEY";

SELECT COUNT(*) FROM "AUTH_KEY"
WHERE EMAIL = #{email}
AND CODE = #{inputKey}
;

signUp.jsp

 <form action="/member/signUp" method="POST" name="signUpFrm" id="signUpFrm">


                <!-- 이메일 입력 -->
                <label for="memberEmail">
                    <span class="required">*</span> 아이디(이메일)
                </label>


                <div class="signUp-input-area">
                    <input type="text" name="memberEmail" id="memberEmail"
                    placeholder="아이디(이메일)" maxlength="30" autocomplete="off">
                   
                    <button id="sendAuthKeyBtn" type="button">인증번호 받기</button>
                </div>
                <span class="signUp-message" id="emailMessage">메일을 받을 수 있는 이메일을 입력해주세요.</span>






                <!-- 인증번호 입력 -->
                <label for="emailCheck">
                    <span class="required">*</span> 인증번호
                </label>


                <div class="signUp-input-area">
                    <input type="text" name="authKey" id="authKey" s placeholder="인증번호 입력" maxlength="6" autocomplete="off" >
                   
                    <button id="checkAuthKeyBtn" type="button">인증하기</button>
                </div>
                <span class="signUp-message" id="authKeyMessage"></span>
                                <!-- 인증번호가 일치하지 않습니다 -->

            </form>

signUp.js

/*유효성 검사 진행 여부 확인용 객체*/
// => 모든 value가 true인 경우만 회원가입 진행
const checkObj = {
    "memberEmail" : false
    ,"memberPw": false
    , "memberPwConfirm" : false
    , "memberNickname" : false
    , "memberTel" : false
    , "authKey" : false
}
//------------------------이메일인증---------------------------
// 인증번호 발송
const sendAuthKeyBtn = document.getElementById("sendAuthKeyBtn");
const authKeyMessage = document.getElementById("authKeyMessage");
let authTimer;
let authMin = 4;
let authSec = 59;


// 인증번호를 발송한 이메일 저장
let tempEmail;


sendAuthKeyBtn.addEventListener("click", function(){
    authMin = 4;
    authSec = 59;


    checkObj.authKey = false;


    if(checkObj.memberEmail){ // 중복이 아닌 이메일인 경우

        /* fetch() API 방식 ajax */
        fetch("/sendEmail/signUp?email="+memberEmail.value)
        .then(resp => resp.text())
        .then(result => {
            if(result > 0){
                console.log("인증 번호가 발송되었습니다.")
                tempEmail = memberEmail.value;
            }else{
                console.log("인증번호 발송 실패")
            }
        })
        .catch(err => {
            console.log("이메일 발송 중 에러 발생");
            console.log(err);
        });
       
        alert("인증번호가 발송 되었습니다.");

        authKeyMessage.innerText = "05:00";
        authKeyMessage.classList.remove("confirm");


        authTimer = window.setInterval(()=>{


            authKeyMessage.innerText = "0" + authMin + ":" + (authSec<10 ? "0" + authSec : authSec);
           
            // 남은 시간이 0분 0초인 경우
            if(authMin == 0 && authSec == 0){
                checkObj.authKey = false;
                clearInterval(authTimer);
                return;
            }


            // 0초인 경우
            if(authSec == 0){
                authSec = 60;
                authMin--;
            }

            authSec--; // 1초 감소

        }, 1000)


    } else{
        alert("중복되지 않은 이메일을 작성해주세요.");
        memberEmail.focus();
    }
});


// 인증 확인
const authKey = document.getElementById("authKey");
const checkAuthKeyBtn = document.getElementById("checkAuthKeyBtn");


checkAuthKeyBtn.addEventListener("click", function(){


    if(authMin > 0 || authSec > 0){ // 시간 제한이 지나지 않은 경우에만 인증번호 검사 진행
        /* fetch API */
        const obj = {"inputKey":authKey.value, "email":tempEmail}
        const query = new URLSearchParams(obj).toString()
        // inputKey=123456&email=user01
        
        fetch("/sendEmail/checkAuthKey?" + query)

        .then(resp => resp.text())
        .then(result => {
            if(result > 0){
                clearInterval(authTimer);
                authKeyMessage.innerText = "인증되었습니다.";
                authKeyMessage.classList.add("confirm");
                checkObj.authKey = true;


            } else{
                alert("인증번호가 일치하지 않습니다.")
                checkObj.authKey = false;
            }
        })
        .catch(err => console.log(err));




    } else{
        alert("인증 시간이 만료되었습니다. 다시 시도해주세요.")
    }


});

EmailController.java 클래스

package edu.kh.project.member.controller;


import java.util.Map;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttributes;


import edu.kh.project.member.model.service.EmailService;


@Controller
@RequestMapping("/sendEmail")
@SessionAttributes("authKey")
public class EmailController {
    
    @Autowired
    private EmailService service;
    
    @GetMapping("/signUp")
    @ResponseBody
    public int signUp(String email) {
        return service.signUp(email, "회원 가입");
    }
    
    
    @GetMapping("/checkAuthKey")
    @ResponseBody
    public int checkAuthKey(@RequestParam Map<String, Object> paramMap){


       System.out.println(paramMap); // {inputKey=wc3rxG, email=knbdh@nate.com}
        
        return service.checkAuthKey(paramMap);
    }
    
}

EmailService.java 인터페이스

package edu.kh.project.member.model.service;


import java.util.Map;


public interface EmailService {
    
   int signUp(String email, String title);
   
   String createAuthKey();


   int checkAuthKey(Map<String, Object> paramMap);
}

EmailServiceImpl.java 인터페이스

package edu.kh.project.member.model.service;


import java.util.HashMap;
import java.util.Map;


import javax.mail.Message;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import edu.kh.project.member.model.dao.EmailDAO;


@Service
public class EmailServiceImpl implements EmailService {


    @Autowired
    private EmailDAO dao;
    
    @Autowired
    private JavaMailSender mailSender;
    
    private String fromEmail = "이메일";
    private String fromUsername = "수업용프로젝트";


    @Override
    public String createAuthKey() {
        String key = "";
        for(int i=0 ; i< 6 ; i++) {
            
            int sel1 = (int)(Math.random() * 3); // 0:숫자 / 1,2:영어
            
            if(sel1 == 0) {
                
                int num = (int)(Math.random() * 10); // 0~9
                key += num;
                
            }else {
                
                char ch = (char)(Math.random() * 26 + 65); // A~Z
                
                int sel2 = (int)(Math.random() * 2); // 0:소문자 / 1:대문자
                
                if(sel2 == 0) {
                    ch = (char)(ch + ('a' - 'A')); // 소문자로 변경
                }
                
                key += ch;
            }
            
        }
        return key;
    }


    @Transactional
    @Override
    public int signUp(String email, String title) {
        
        //6자리 난수 인증번호 생성
        String authKey = createAuthKey();
        try {


            //인증메일 보내기
            MimeMessage mail = mailSender.createMimeMessage();
            
            // 제목
            String subject = "[Board Project]"+title+" 인증코드";
            
            // 문자 인코딩
            String charset = "UTF-8";
            
            // 메일 내용
            String mailContent 
                = "<p>Board Project "+title+" 인증코드입니다.</p>"
                + "<h3 style='color:blue'>" + authKey + "</h3>";
            
            
            
            // 송신자(보내는 사람) 지정
            mail.setFrom(new InternetAddress(fromEmail, fromUsername));
            mail.addRecipient(Message.RecipientType.TO, new InternetAddress(email));
            
            // 수신자(받는사람) 지정
            
            // 이메일 제목 세팅
            mail.setSubject(subject, charset);
            
            // 내용 세팅
            mail.setText(mailContent, charset, "html"); //"html" 추가 시 HTML 태그가 해석됨
            
            mailSender.send(mail);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
        
        Map<String, String> map = new HashMap<String, String>();
        map.put("authKey", authKey);
        map.put("email", email);
        
        System.out.println(map);
        
        int result = dao.updateAuthKey(map);
        // 이전에 이메일 인증을 한 이력이 있을 때
        
        // 처음 인증 할 때 
        if(result == 0) {
           result = dao.insertAuthKey(map);
        }
        


        return result;
    }


   @Override
   public int checkAuthKey(Map<String, Object> paramMap) {
      return dao.checkAuthKey(paramMap);
   }      
   
   // 테스트
}

EmailDAO.java

package edu.kh.project.member.model.dao;

import java.util.Map;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class EmailDAO {
    
   @Autowired
    private SqlSessionTemplate sqlSession;

   public int updateAuthKey(Map<String, String> map) {
      return sqlSession.update("emailMapper.updateAuthKey", map);
   }

   public int insertAuthKey(Map<String, String> map) {
      return sqlSession.update("emailMapper.insertAuthKey", map);
   }

   public int checkAuthKey(Map<String, Object> paramMap) {
      return sqlSession.selectOne("emailMapper.checkAuthKey", paramMap);
   }
}

email-mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="emailMapper">

   <update id="updateAuthKey">
      UPDATE  "AUTH_KEY" SET
      CODE = #{authKey},
      CREATE_TIME = sysdate
      WHERE EMAIL = #{email}
   </update>
   
   <insert id="insertAuthKey">
      INSERT INTO "AUTH_KEY" VALUES(SEQ_AUTH_KEY_NO.NEXTVAL, #{authKey}, #{email}, DEFAULT)
   </insert>
   
   <select id="checkAuthKey" resultType="int">
      SELECT COUNT(*) FROM "AUTH_KEY"
      WHERE EMAIL = #{email}
      AND CODE = #{inputKey}
   </select>
</mapper>
profile
나를 죽이지 못하는 오류는 내 코드를 더 강하게 만들지ㅋ

0개의 댓글