Spring Boot Board Project_06 이메일 인증 (비동기)

송지윤·2024년 4월 19일
0

Spring Framework

목록 보기
38/65

1. 인증번호 받기 버튼 클릭 시 함수 정의 한 부분에서 fetch() 요청 보내기

    fetch("/email/signup", {
        method : "POST",
        headers : {"Content-Type" : "application/json"},
        body : memberEmail.value
    })

@Autowired 를 이용한 의존성 주입 방법은 3가지 존재

  1. 필드
  2. setter
  3. 생성자 (권장)

1. 필드에 의존성 주입하는 방법

@Autowired
private EmailService service;

의존성 주입(DI)

2. setter 이용

private EmailService service;

@Autowired
public void setService(EmailService service) {
	this.service = service;
}

3. 생성자 (권장)

private EmailService es;
private MemberService ms;

@Autowired
public EmailController(EmailService es, MemberService, MemberService ms) {
	this.es = es;
    this.ms = ms;
}

생성자에 Autowired 자동으로 붙여주는 어노테이션 (Lombok 라이브러리에서 제공)

@RequiredArgsConstructor

@RequiredArgsConstructor 를 이용하면 필드 중
1. 초기화 되지 않은 final이 붙은 필드
2. 초기화 되지 않은 @NotNull 이 붙은 필드
위 둘에 해당하는 필드에 대한 @Autowired 생성자 구문을 자동 완성해주는 어노테이션(내부적으로)

2. 받아줄 Controller 클래스 생성 및 Service 호출

EmailController 권장되는 방식으로 @Autowired 생성되는 거

@Controller
@RequestMapping("email")
@RequiredArgsConstructor
public class EmailController {

	private final EmailService service;
    
	@ResponseBody
	@PostMapping("signup")
	public int signup(@RequestBody String email) {
		
		String authKey = service.sendEmail("signup", email);
		
		return 0;
	}
}

service 호출할 때 보낸 "signup"은 key 역할을 할 문자열
이메일 보낼 때 html 형식으로 보내줄건데 그 html 이름을 signup이라고 해주겠다는 키

3. Service 인증키 난수 생성하는 메서드 작성

EmailServiceImpl

	/** 인증번호 생성 (영어 대문자 + 소문자 + 숫자 6자리)
	 * @return authKey
	 */
	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;
	}

구글 SMTP, EmailConfig

Google SMTP를 이용한 이메일 전송하기

  • SMTP (Simple Mail Transfer Protocol) 간단한 메일 전송 규약
    --> 이메일 메세지를 보내고 받을 때 사용하는 약속(규약, 방법)

  • Google SMTP

Java Mail Sender 모듈 필요 (build.gradle 모듈 추가해둠)
implementation 'org.springframework.boot:spring-boot-starter-mail'

Java Mail Sender -> Google SMTP -> 대상에게 이메일 전송

Java Mail Sender 에 Google SMTP 이용 설정 추가

  1. config.properties 내용 추가(계정, 앱비밀번호)
  2. EmailConfig.java (Java Mail Sender에 대한 설정해줘야함)

config.properties

Google SMTP에 사용할 username, password

spring.mail.username=계정이메일
spring.mail.password=앱비밀번호

앱비밀번호는 공백없이 작성

EmailConfig 클래스

@Value : properties 에 작성된 내용 중 키가 일치하는 값을 얻어와 필드에 대입
org.springframework.beans.factory.annotation.Value;

import java.util.Properties;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

@Configuration
@PropertySource("classpath:/config.properties")
public class EmailConfig {

	@Value("${spring.mail.username}")
	private String userName;
	
	@Value("${spring.mail.password}")
	private String password;
	
	@Bean
	public JavaMailSender javaMailSender() {
		
		JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
		
		// mailSender 에서 이용할 각종 설정들 작성
		Properties prop = new Properties();
		// 전송 프로토콜 설정
		prop.setProperty("mail.transport.protocol", "smtp");
		// SMTP 서버 인증 사용할지 말지
		prop.setProperty("mail.smtp.auth", "true");
		// 안정한 연결 활성화 할지 말지
		prop.setProperty("mail.smtp.starttls.enable", "true");
		// mail 보낼 때 debug 사용 여부
		prop.setProperty("mail.debug", "true");
		// 신뢰할 수 있는 서버 주소 사용 smtp.gmail.com
		prop.setProperty("mail.smtp.ssl.trust","smtp.gmail.com");
		// 버전
		prop.setProperty("mail.smtp.ssl.protocols","TLSv1.2");
		
		// 구글 smtp 사용자 계정
		mailSender.setUsername(userName);
		mailSender.setPassword(password);
		// smtp 서버 호스트 설정
		mailSender.setHost("smtp.gmail.com");
		// SMTP 포트 587
		mailSender.setPort(587);
		mailSender.setDefaultEncoding("UTF-8");
		mailSender.setJavaMailProperties(prop);
		
		return mailSender;
	}
}

SMTP, EmailConfig 설정 완료 후 ServiceImpl에 의존성 주입

EmailServiceImpl

	// EmailConfig 설정이 적용된 객체(메일 보내기 기능)
	private final JavaMailSender mailSender;

JavaMailSender 는 위에 EmailConfig에 만들어둠

4. email 로 보낼 html 작성

resources/templates/email/signup.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<!-- 메일은 header 불필요 -->
<body>
    <!-- css/js 파일은 이메일 첨부 불가 -->
    <!-- 태그 안에 inline 형식으로 작성 -->
    <div style="display: flex; flex-direction: column; align-items: center;">
    
        <!-- Content-ID로 등록된 이미지 중 logo 출력 -->
        <img src="cid:logo" width="200px">

        <h3>
            아래 발급된 인증 번호를 제한 시간 내에
            <br>
            회원 가입 화면에 입력 후 인증 버튼을 눌러주세요.
        </h3>

        <h3 style="text-align: center; border: 3px solid black;
            color: blue; width: 400px; padding: 10px;">
            <th:block th:text="${authKey}"></th:block>    
        </h3>
    </div>
</body>
</html>

th:text 안에 authKey 는 serviceImpl 에서 세팅해줄 거

5. ServiceImpl 에서 HTML에 값 세팅, 어떤 html을 해석하는지 알려주는 메서드 작성

타임리프(템플릿 엔진)를 이용해서 html 코드 -> java 코드로 변환
private final SpringTemplateEngine templateEngine;
필드에 작성

	// HTML 파일을 읽어와 String 으로 변환 (타임리프 적용)
	private String loadHtml(String authKey, String htmlName) {

		// org.thymeleaf.Context. 선택
		Context context = new Context();
		// thymeleaf 가 적용된 html 상에서 값을 세팅할 수 있는 객체
		
		// 타임리프가 적용된 HTML 에서 사용할 값을 추가
		context.setVariable("authKey", authKey);
		
		// 어떤 HMTL 을 해석하고 있는지도 알려줘야함
		return templateEngine.process("email/" + htmlName, context);
		// templates/email 폴더에서 htmlName 과 같은 .html 파일 내용을 읽어와서 String 으로 변환
		// , context 는 authKey 세팅해둔 거 보내주는 거
	}

6. 인증 이메일 보내기

EmailServiceImpl

	// 이메일 보내기
	@Override
	public String sendEmail(String htmlName, String email) {

		// 6자리 난수 생성
		String authKey = createAuthKey();
		
		// 이메일 보낼 때 Exception 발생할 수 있어서 try catch
		try {
			
			// 메일 제목
			String subject = null;
			
			// 키에 따라 메일 내용 다르게 보내기 위해
			switch(htmlName) {
			case "signup" :
				subject = "[boardProject] 회원 가입 인증번호 입니다."; break;
			}
			
			// 인증 메일 보내기
			
			// MimeMessage (객체) : Java 에서 메일을 보내는 객체
			MimeMessage mimeMessage = mailSender.createMimeMessage();
			
			// MimeMessageHelper : Spring 에서 제공하는 메일 발송 도우미(간단 + 타임리프)
			MimeMessageHelper helper
				= new MimeMessageHelper(mimeMessage, true, "UTF-8");
			// 1번 매개변수 : MimeMessage 객체
			// 2번 매개변수 : 파일 전송 사용 할거냐(true) 말거냐(false)
			// 3번 매개변수 : 문자 인코딩 지정
			
			helper.setTo(email); // 받는 사람 이메일 지정 (회원가입하는사람)
			helper.setSubject(subject); // 이메일 제목 지정
			
			 // html
			helper.setText( loadHtml(authKey, htmlName), true);
			// HTML 코드 해석 여부 true (innerHTML 해석)
			
			// CID(Content-ID)를 이용해 메일에 이미지 첨부
			// (파일첨부와는 다름, 이메일 내용 자체에 사용할 이미지 첨부)
			// logo 추가예정
			helper.addInline("logo", new ClassPathResource("static/images/logo.jpg")); // html 에서 cid:logo 로 부름
			// -> 로고 이미지를 메일 내용에 첨부하는데
			//    사용하고 싶으면 "logo"라는 id 를 작성해라
			// classpath 는 resources	
			
			// 메일 보내기
			mailSender.send(mimeMessage);
			
			log.debug("인증번호 : " + authKey);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
		
		7번 내용
	}

메일 받은 화면

7. 이메일과 인증번호 테이블에 저장 후 Controller 로 값 돌려주기

EmailServiceImple

		// 이메일 + 인증번호를 "TB_AUTH_KEY" 테이블 저장
		Map<String, String> map = new HashMap<>();
		map.put("authKey", authKey);
		map.put("email", email);
		
		// 1) 해당 이메일이 DB에 존재하는 경우 (인증번호 받은 이력이 있는 경우)
		//    수정(update)을 먼저 진행
		//    -> 1 반환 == 업데이트 성공 == 이미 존재해서 인증번호 변경됐다.
		//    -> 0 반환 == 업데이트 실패 == 이메일이 테이블에 존재 X (처음 인증번호 받음) --> INSERT 시도
		
		int result = mapper.updateAuthKey(map);
		
		if(result == 0) {
			// 업데이트 실패 (인증번호 처음) INSERT 시도
			result = mapper.insertAuthKey(map);
			
		}
		
		// INSERT 후 result 가 0이면 실패, 1이면 성공
		if(result == 0) return null;
		
		// 성공
		return authKey; // 오류없이 완료되면 인증번호 반환
	}

email-mapper.xml

	<!-- 인증번호 수정 -->
	<update id="updateAuthKey">
		UPDATE "TB_AUTH_KEY" SET 
		"AUTH_KEY" = #{authKey},
		"CREATE_TIME" = SYSDATE 
		WHERE "EMAIL" = #{email}
	</update>
	
	<!-- 인증번호 삽입 -->
	<insert id="insertAuthKey">
		INSERT INTO "TB_AUTH_KEY" 
		VALUES(SEQ_KEY_NO.NEXTVAL, #{email}, #{authKey}, DEFAULT)
	</insert>

8. Controller 에서 값 전달 받아서 js로 돌려주기

EmailController

		if(authKey != null) { // 인증번호가 반환돼서 돌아옴
			// 이메일 보내기 성공
			return 1;
		}
		
		// 이메일 보내기 실패
		return 0;

9. js에서 값 돌려 받아서 처리

signup.js
fetch() 아래

    .then(resp => resp.text())
    .then(result => {
        if(result == 1) {
            console.log("인증 번호 발송 성공");
        } else {
            console.log("인증 번호 발송 실패");
        }
    });

결과 확인

인증번호 검사

인증번호 입력 후 인증하기 버튼 누르면 DB 에 들어온 번호와 같은지 확인하고 처리

1. 입력 받은 이메일, 인증번호 fetch() 로 Controller 에 보내주기 (후처리 미리 작성)

signup.js

checkAuthKeyBtn.addEventListener("click", () => {

  	// 타이머가 00:00 인 경우
    if(min === 0 && sec === 0) {
        alert("인증번호 입력 제한 시간을 초과하였습니다.");
        return;
    }

    // 인증번호가 제대로 입력되지 않은 경우
    if(authKey.value.length < 6) {
        alert("인증번호를 정확히 입력해주세요");
        return;
    }

    // 입력받은 이메일, 인증번호로 객체 생성
    const obj = {
        "email" : memberEmail.value,
        "authKey" : authKey.value
    };

    fetch("/email/checkAuthKey", {
        method : "POST",
        headers : {"Content-Type" : "application/json"},
        body : JSON.stringify(obj) // obj를 JSON으로 변경
    })
    .then(resp => resp.text())
    .then(result => {

        if(result == 0) {
            alert("인증번호가 일치하지 않습니다.");
            checkObj.authKey = false;
            return;
        }

        clearInterval(authTimer); // 타이머 멈춤

        authKeyMessage.innerText = "인증 되었습니다.";
        authKeyMessage.classList.remove('error');
        authKeyMessage.classList.add('confirm');

        checkObj.authKey = true; // 인증 번호 검사여부 true
    });
});

2. Controller 에서 값 받아서 service 호출 및 sql 작성 후 값 돌려주기

EmailController

	@ResponseBody
	@PostMapping("checkAuthKey")
	public int checkAuthKey(@RequestBody Map<String, Object> map) {
		
		// 입력 받은 이메일, 인증번호가 DB에 있는지 조회
		// 이메일 있고, 인증번호 일치 == 1
		// 아니면 0
		return service.checkAuthKey(map);
	}

EmailServiceImpl

	@Override
	public int checkAuthKey(Map<String, Object> map) {
		return mapper.checkAuthKey(map);
	}

EmailMapper interface

int checkAuthKey(Map<String, Object> map);

email-mapper.xml

	<select id="checkAuthKey" resultType="_int">
		SELECT COUNT(*)
		FROM "TB_AUTH_KEY"
		WHERE EMAIL = #{email}
		AND AUTH_KEY = #{authKey}
	</select>

결과 확인
업로드중..

0개의 댓글