SMTP

oYJo·2025년 3월 13일

Spring

목록 보기
19/29
post-thumbnail

SMTP

SMTP는 Simple Mail Transfer Protocol의 약자입니다
원래는 이걸 해야하는데 이건 지금 하기에 시간이 부족하다

구글API

2단계 인증 진행하고


↑ 앱 비밀번호
구글 smtp 이용할 때 로그인, 비밀번호를 하는데 보안을 위해서 앱 비밀번호 나옴

사용할 때 띄어쓰기 없애고 집어넣기

1️⃣ Oracle

--이메일 인증키 테이블
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}
;

2️⃣ Xml

✔️pom.xml

<!-- 이메일 인증 -->
<!-- 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>

✔️resources(spring) → email-context.xml

<?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="ohsuzy1218@gmail.com"/>
    <property name="password" value="qjuq jyju vxps zvnq" />
    <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

classpath:spring/email-context.xml

그냥 xml 만들었다고 bean 등록하지 않음
등록해줘야한다


3️⃣ Visual Studio Code

✔️webapp(WEB-INF(views(member))) → signUp.jsp

...
<!-- 이메일 입력 -->
<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>
<!-- 인증번호가 일치하지 않습니다 -->
...

✔️webapp(resources(js(signUp))) → signUp.js

// 인증번호 발송
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()

    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("인증 시간이 만료되었습니다. 다시 시도해주세요.")
  }

});
// 인증번호 발송
const sendAuthKeyBtn = document.getElementById("sendAuthKeyBtn");
const authKeyMessage = document.getElementById("authKeyMessage");
let authTimer;
let authMin = 4;
let authSec = 59;

각각 sendAuthKeyBtn, authKeyMessage를 id로 받아온다
let authTimer; : 타이머를 생성해준다
let authMin = 4;
let authSec = 59;

// 인증번호를 발송한 이메일 저장
let tempEmail;
sendAuthKeyBtn.addEventListener("click", function(){
    authMin = 4;
    authSec = 59;
    checkObj.authKey = false;

전역변수로 authMin, authSec을 설정하고
checkObj.authKey를 활용해서 기본 false로 만든다
그리고 이메일이 중복이 아니면

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);
        });

비동기 방법으로 sendEmail/signUp?email로 요청주소를 설정하고
따로 아무것도 안 적혀 있기 때문에 get방식으로 보내준다

파싱을 2가지로 할 수 있는데 text, json으로 할 수 있고, text로 파싱한다
선물 포장지를 풀고 나온 내용물 = result
이 result를 가지고 뭘 할건지를 {} 안에 넣어준다


여기서는 result가 성공하면 인증번호가 발송되었습니다. 하고 tempEmail 안에 memberEmail.value를 집어넣는다
성공하지 못하면 인증번호 발송 실패를 보내준다


.catch는 예외처리에 대한 내용으로 콘솔 즉 화면에 이메일 발송 중 에레 발생이라고 뜨게 한다
비동기 방식이 끝나면 인증번호가 발송되었다는 알림창이 뜨며
authKeyMessage 쪽에 text 05:00 표시가 뜨며 ClassList confirm을 지워준다

💡 Element.classList 는 엘리먼트의 클래스 속성의 컬렉션인 활성 DOMTokenList 를 반환하는 읽기 전용 프로퍼티이다

authTimer = window.setInterval(()=>{
💡 window.setTimeout : 일정 시간 간격 이후에 함수가 한번 실행된다
window.setInterval : 일정 시간 간격으로 함수가 주기적으로 실행된다

삼항연산자를 사용해서 초가 10보다 이하면 0초라고 뜨고
아니면 해당초를 보여준다
💡 조건 ? 참 : 거짓

if(authMin == 0 && authSec == 0){
  checkObj.authKey = false;
  clearInterval(authTimer);
  return;
}

0분 0초일 때 false가 나오고,

💡 전역 clearInterval() 메서드는 이전에 setInterval()을 호출하여 생성한 타이머에 의해 반복되는 작업을 취소

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

0초이면 60초(1분)로 띄우고 -1분씩 감소시킨다
결과적으로, 이 코드는 authSec가 0이 될 때마다 authSec를 60으로 초기화하고, authMin을 1 감소시켜 시간을 조정하는 역할을 합니다. 예를 들어, 1분이 경과하면 초를 60으로 다시 설정하고, 분을 하나 줄이는 식입니다.

new URLSearchParams(obj).toString()
URLSearchParams 인터페이스의 toString() 메서드는 URL에 사용할 수 있는 쿼리 문자열을 반환합니다.


4️⃣ Spring

✔️project(member(controller)) → 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);
    }
    
}

✔️project(member(mode(service))) → 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 = "ohsuzy1218@gmail.com";
    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);
   }      
   
}

✔️project(member(model(dao))) → 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);
   }
}

💡 일반적인 GET/POST 요청 파라미터라면 @RequestBody를 사용할 일이 없지만, xml, json기반 메시지 사용하는 요청 경우 유용함

profile
Hello! My Name is oYJo

0개의 댓글