so woon!

이메일 찾기 구현 본문

Spring Boot/구현해보기

이메일 찾기 구현

xowoony 2022. 11. 13. 20:56

recoverEmail.html

<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>또치뱅크 - 이메일 찾기</title>
    <th:block th:replace="~{fragments/head :: common}"></th:block>
    <script defer th:src="@{/member/resources/scripts/recoverEmail.js}"></script>
    <link rel="stylesheet" th:href="@{/member/resources/stylesheets/recoverEmail.css}">
</head>
<body>
    <th:block th:replace="~{fragments/body :: header}"></th:block>
    <th:block th:replace="~{fragments/body :: cover}"></th:block>
    <main class="--main main">
        <form name="form" id="form">
            <div class="title-container">
                <h1 class="title">이메일 찾기</h1>
            </div>
            <table class="table">
                <tbody>
                <!--이름 입력창-->
                <tr>
                    <th rowspan="1" id="name">이름</th>
                    <td>
                        <label class="label name">
                            <input autofocus class="--object-input input" maxlength="50" name="name"
                                   placeholder="이름을 입력해 주세요." type="text">
                        </label>
                    </td>
                </tr>
                <!-- 연락처 입력창-->
                <tr>
                    <th rowspan="1" id="contact">연락처</th>
                    <td>
                        <label class="label contact">
                            <span hidden>연락처</span>
                            <input autofocus class="--object-input input" maxlength="50" name="contact"
                                   placeholder="연락처를 입력해 주세요." type="text">
                        </label>
                    </td>
                </tr>
                <!--이메일 찾기 버튼-->
                <tr>
                    <th id="login"></th>
                    <td>
                        <label>
                            <input class="login-button" id="login-button" name="loginButton"
                                   type="submit" value="이메일 찾기" href="#">
                        </label>
                    </td>
                </tr>
                <!--입력하신 이메일은 ~입니다.-->
                <tr class="emailAlert" rel="emailAlert">
                    <th></th>
                    <td>
                    <span>
                        <span class="emailAlertText"></span>
                    </span>
                    </td>
                </tr>

                <!--로그인하러 가기-->
                <tr class="warning-login">
                    <th></th>
                    <td>
                        <label class="warningLogin">
                            <input class="goLoginText" id="goLoginText" name="goLoginText"
                                   type="submit" value="로그인하러 가기" onclick="location.href='login'">
                        </label>
                    </td>
                </tr>

                <!--경고창 - 이름 or 연락처를 입력해주세요-->
                <tr class="warning-row" rel="warningRow">
                    <th></th>
                    <td>
                    <span class="warning">
                        <i class="icon fa-solid fa-triangle-exclamation"></i>
                        <span class="text"></span>
                    </span>
                    </td>
                </tr>
                </tbody>
            </table>
        </form>
    </main>
    <th:block th:replace="~{fragments/body :: footer}"></th:block>
</body>
</html>

recoverEmail.css

@charset "UTF-8";
/*큰 틀*/
#form {
    width: 100%;
    max-width: 40rem;
    align-items: stretch;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    margin: 5rem 2rem;
}
#form > .title-container {
    margin-bottom: 3rem;
}
#form > .title-container > .title {
    font-size: 2rem;
    font-weight: 500;
}
#form > .table {
    width: 100%;
    max-width: 30rem;
    align-self: center;
    border: none;
    border-collapse: collapse;
}
#form > .table th {
    padding-right: 1rem;
    white-space: nowrap;
}
#form > .table td {
    width: 100%;
}

/*이메일 찾기 버튼*/
#form > .table > tbody > tr > td > label > #login-button {
    background-color: rgb(40, 117, 225);
    color: rgb(255, 255, 255);
    width: 100%;
    margin-top: 0.25rem;
    padding: 1rem 1rem;
    border-radius: 0.3rem;
}
/*경고창*/
#form > .table .warning-row {
    display: none;
}
#form > .table .warning-row.visible {
    display: table-row;
}
#form > .table .warning-row .warning {
    background-color: rgb(231, 76, 60);
    border-radius: 0.375rem;
    color: rgb(255, 255, 255);
    font-size: 1rem;
    margin-top: 0.25rem;
    padding: 1rem 1.25rem;

    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
}
#form > .table .warning-row .warning > .icon {
    font-size: 1.5rem;
    margin: 0 1.25rem 0 0.25rem;
}
#form > .table .warning-row .warning > .text {
    flex: 1;
    text-align: justify;
}


/* 입력하신 뭐시기*/
#form > .table > tbody > .emailAlert > td > span > .emailAlertText {
    display: none;
}
#form > .table > tbody > .emailAlert > td > span > .emailAlertText.visible {
    background-color: rgb(40, 117, 225);
    color: rgb(255, 255, 255);
    width: 100%;
    margin-top: 0.25rem;
    padding: 1rem 1rem;
    border-radius: 0.3rem;

    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: center;

    flex: 1;
    text-align: justify;
}


/*로그인하러가기*/
#form > .table > tbody > tr > td > label > .goLoginText {
    display: none;
}

#form > .table > tbody > tr > td > label > .goLoginText.visible {
    background-color: rgb(40, 117, 225);
    color: rgb(255, 255, 255);
    width: 100%;
    margin-top: 0.25rem;
    padding: 1rem 1rem;
    border-radius: 0.3rem;

    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: center;

    flex: 1;
    text-align: justify;
}


/* footer */
.--footer {
    background-color: #191f28;
    color: #b0b8c1;
}

recoverEmail.js

const form = window.document.getElementById('form');

const Warning = {
    getElementById: () => form.querySelector('[rel="warningRow"]'),
    show: (text) => {
        const warningRow = Warning.getElementById();
        warningRow.querySelector('.text').innerText = text;
        warningRow.classList.add('visible');
    },
    hide: () => Warning.getElementById().classList.remove('visible')
};

let text = form.querySelector('.text');
let emailAlertText = form.querySelector('.emailAlertText');
let goLoginText = form.querySelector('.goLoginText');

form.onsubmit = (e) => {
    e.preventDefault();
    Warning.hide();
    if (form['name'].value === '') {
        form.querySelector('.warning-row').classList.add('visible');
        text.innerText = '이름을 입력해 주세요.';
        form['name'].focus();
        return false;
    }
    if (form['contact'].value === '') {
        form.querySelector('.warning-row').classList.add('visible');
        text.innerText = '연락처를 입력해 주세요.';
        form['contact'].focus();
        return false;
    }

    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('name', form['name'].value);
    formData.append('contact', form['contact'].value);

    xhr.open('POST', '/member/recoverEmail');
    xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
            if (xhr.status >= 200 && xhr.status < 300) {
                const responseObject = JSON.parse(xhr.responseText);
                switch (responseObject['result']) {
                    case 'success':
                        form.querySelector('.emailAlertText').classList.add('visible');
                        emailAlertText.innerText = '입력하신 이름과 연락처를 가진 회원의 이메일은\n ? \n입니다.';
                        form.querySelector('.goLoginText').classList.add('visible');
                        break;
                    default:
                        Warning.show('입력한 정보와 일치하는 회원이 없습니다.');
                        break;
                }
            } else {
                Warning.show('서버와 통신하지 못하였습니다.\n잠시 후 다시 시도해 주세요.');
            }
        }
    };
    xhr.send(formData);
}

MemberControllor.java

package dev.xowoony.studymemberbbs.controllers;

import dev.xowoony.studymemberbbs.entities.member.EmailAuthEntity;
import dev.xowoony.studymemberbbs.entities.member.UserEntity;
import dev.xowoony.studymemberbbs.enums.CommonResult;
import dev.xowoony.studymemberbbs.interfaces.IResult;
import dev.xowoony.studymemberbbs.services.MemberService;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.mail.MessagingException;
import java.security.NoSuchAlgorithmException;


@Controller
// 컨트롤러임을 명시한다.
@RequestMapping(value = "/member")
// 맵핑, 주소값은 localhost:8080/member
public class MemberController {
    // 컨트롤러와 서비스간 의존성을 주입한다.
    private final MemberService memberService;
    @Autowired
    //요구되는 타입을 스프링부트가 알아서 객체화 하여 전달토록 한다. (컨트롤러-서비스 간) 의존성 주입을 위해 사용.
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
    // MemberController 클래스를 생성한다.
    @RequestMapping(value = "register", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
    //맵핑, 주소값은 localhost:8080/member/register임, GET(주소창 입력) 방식으로 요청, 응답을 MediaType의 TEXT_HTML_VALUE로 돌려주겠다.
    public ModelAndView getRegister() {
        ModelAndView modelAndView = new ModelAndView("member/register");
        //ModelAndView를 객체화 한다.
        return modelAndView;
        // modelAndView를 반환한다.
    }
    @RequestMapping(value = "email",
            method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    // 응답을 JSON으로 해석해야 한다는 조언이다.
    // 맵핑, 주소값은 localhost:8080/member/email임, POST방식으로 요청한다. 응답을 MediaType의 APPLICATION_JSON_VALUE로 돌려주겠다.
    @ResponseBody
    public String postEmail(UserEntity user, EmailAuthEntity emailAuth) throws NoSuchAlgorithmException, MessagingException {
        Enum<? extends IResult> result = this.memberService.sendEmailAuth(user, emailAuth);
        // ?만 적어줘도 됨.
        // 브라우저에게 salt 값을 넘겨줘야 되는데 user,까지 적어주면 솔트값을 못전해줌 emailAuth에서 솔트 지정해주면 그 값이 넘어오기 때문에 emailAuth를 꼭 써주어야 한다.
        JSONObject responseObject = new JSONObject();
        responseObject.put("result", result.name().toLowerCase());
        // result의 name메서드를 불러와 소문자화 하겠다.
        if (result == CommonResult.SUCCESS) {
            responseObject.put("salt", emailAuth.getSalt());
        }
        //F12에 보면 솔트값도 넘어옴. DB에도 찍힌다.
        return responseObject.toString();
        // {
        //  "result":"success"
        // } 를 브라우저가 반환한다. 솔트값을 추가로 알려줌. result가 success일때만 반환.
        // 소문자화 하는 이유는 자바로 개발했다는 걸 숨기기 위함임.
    }
    @RequestMapping(value = "register",
            method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public String postRegister(UserEntity user, EmailAuthEntity emailAuth) throws NoSuchAlgorithmException {
        Enum<?> result = this.memberService.register(user, emailAuth);
        // 1. 'MemberService' 가 가진 'register' 메서드에 'user' 및 'emailAuth' 전달하여 호출하기.
        JSONObject responseObject = new JSONObject();
        // 2. <1>이 반환하는 결과 'Enum<?>'를 받아와 'JSONObject' 타입의 응답 결과 만들기.
        responseObject.put("result", result.name().toLowerCase());
        //"result" 는 자바스크립트에도 switch문에 바꿔야 하고
        if (result == CommonResult.SUCCESS) {
            responseObject.put("salt", emailAuth.getSalt());
        }
        return responseObject.toString();
        // 3. <2>에서 만들어진 'JSONObject' 객체를 문자열화(toString) 하여 반환하기.
    }
    @RequestMapping(value = "email",
            method = RequestMethod.PATCH,
            produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public String patchEmail(EmailAuthEntity emailAuth) {
        Enum<?> result = this.memberService.verifyEmailAuth(emailAuth);
        JSONObject responseObject = new JSONObject();
        responseObject.put("result", result.name().toLowerCase());
        return responseObject.toString();
    }
    
    // 비밀번호 재설정하기
    @RequestMapping(value = "recoverPassword",
            method = RequestMethod.GET,
            produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView getRecoverPassword() {
        ModelAndView modelAndView = new ModelAndView("member/recoverPassword");
        return modelAndView;
    }
    @RequestMapping(value = "recoverPassword",
            method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody 
    public String postRecoverPassword(EmailAuthEntity emailAuth) throws MessagingException {
        Enum<?> result = this.memberService.recoverPasswordSend(emailAuth); // this.이 자리에 SUCCESS 또는 FAILURE 로 바뀜쓰
        JSONObject responseObject = new JSONObject();
        responseObject.put("result", result.name().toLowerCase());
        if (result == CommonResult.SUCCESS) {
            responseObject.put("index", emailAuth.getIndex());
        }
        return responseObject.toString(); // "{"result":"success"}"
    }
    @RequestMapping(value = "recoverPasswordEmail",
            method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public String postRecoverPasswordEmail(EmailAuthEntity emailAuth) {
        Enum<?> result = this.memberService.recoverPasswordcheck(emailAuth);
        JSONObject responseObject = new JSONObject();
        responseObject.put("result", result.name().toLowerCase());
        if (result == CommonResult.SUCCESS) {
            responseObject.put("code", emailAuth.getCode());
            responseObject.put("salt", emailAuth.getSalt());
        }
        return responseObject.toString();
    }
    @RequestMapping(value = "recoverPasswordEmail",
            method = RequestMethod.GET,
            produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView getRecoverPasswordEmail(EmailAuthEntity emailAuth) {
        Enum<?> result = this.memberService.recoverPasswordAuth(emailAuth);
        ModelAndView modelAndView = new ModelAndView("member/recoverPasswordEmail");
        modelAndView.addObject("result", result.name());
        return modelAndView;
    }
    @ResponseBody
    public JSONObject recoverPasswordAuth(EmailAuthEntity emailAuth) {
        Enum<? extends IResult> result = this.memberService.recoverPasswordAuth(emailAuth);
        JSONObject responseObject = new JSONObject();
        responseObject.put("result", result.name().toLowerCase());
        if (result == CommonResult.SUCCESS) {
            responseObject.put("code", emailAuth.getCode());
            responseObject.put("salt", emailAuth.getSalt());
        }
        return null;
    }
    @RequestMapping(value = "recoverPassword",
            method = RequestMethod.PATCH,
            produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public String patchRecoverPassword(EmailAuthEntity emailAuth, UserEntity user) {
        Enum<?> result = this.memberService.recoverPassword(emailAuth, user);
        JSONObject responseObject = new JSONObject();
        responseObject.put("result", result.name().toLowerCase());
        return responseObject.toString();
    }
    
	// 로그인
    @RequestMapping(value = "login",
            method = RequestMethod.GET,
            produces = MediaType.TEXT_HTML_VALUE)

    public ModelAndView getLogin() {
        ModelAndView modelAndView = new ModelAndView("member/login");
        return modelAndView;
    }
    @RequestMapping(value = "login",
            method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public String postLogin(UserEntity user) {
        Enum<?> result = this.memberService.login(user);
        JSONObject responseObject = new JSONObject();
        responseObject.put("result", result.name().toLowerCase());
        return responseObject.toString();
    }
    
    // 이메일 찾기
    @RequestMapping(value = "recoverEmail",
            method = RequestMethod.GET,
            produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView getRecoverEmail() {
        ModelAndView modelAndView = new ModelAndView("member/recoverEmail");
        return modelAndView;
    }
    @RequestMapping(value = "recoverEmail",
            method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public String postRecoverEmail(UserEntity user) {
        Enum<?> result = this.memberService.recoverEmail(user);
        JSONObject responseObject = new JSONObject();
        responseObject.put("result", result.name().toLowerCase());
        return responseObject.toString();
    }
}

 

MemberService.java

package dev.xowoony.studymemberbbs.services;

import dev.xowoony.studymemberbbs.entities.member.EmailAuthEntity;
import dev.xowoony.studymemberbbs.entities.member.UserEntity;
import dev.xowoony.studymemberbbs.enums.CommonResult;
import dev.xowoony.studymemberbbs.enums.member.RegisterResult;
import dev.xowoony.studymemberbbs.enums.member.SendEmailAuthResult;
import dev.xowoony.studymemberbbs.enums.member.VerifyEmailAuthResult;
import dev.xowoony.studymemberbbs.interfaces.IResult;
import dev.xowoony.studymemberbbs.mappers.IMemberMapper;
import dev.xowoony.studymemberbbs.utils.CryptoUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

//서비스 로직 구현
// 1. 받은 이메일을 사용하는 유저 레코드가 있는가? 있으면 실패로 결과를 돌려줘야 함. => "이미 사용중인 이메일입니다."
// 2. (Apache Commons Langs 사용하여) 인증번호 및 솔트(Salt)생성 후 테이블에 인서트
// 3. 2에서 생성된 인증번호를 전송 -> "인증 번호를 전송하였습니다. 5분 이내에 입력해 주세요."

// 3개중 하나라도 제대로 안되면 롤백 (트랜젝션을 적용해주어야 함)
//이메일인증이라는 목적 하에 3개가 1개 단위가 된다.
// 서버가 껐다 켜져도 기억하고 있으려면 DB에 저장을 시켜줘야 한다.
// 인증번호를 발송

// 서버 - > 인증번호 -> 이메일로 전송
// 서버는 솔트키를 이용해 클라이언트에게 넘겨줌 랜덤하게 만들어준 키. 어딘가에 숨겨져있다.
// 요청이 들어오면 이메일로 6자리 보내주고 128자 솔트키가 자바스크립트를 통해 회원가입 페이지로 보내준다
// 눌렀을때 솔트랑 인증번호 두개가 넘어옴. 서버로.
// 브라우저쪽으로는 솔트가
// 이메일로는 6자리가 넘어옴
// 솔트, 인증번호 세트가 서버로 넘어옴
//솔트가 빠지면 인증번호로만 인증을 시켜줘야되는 일이 발생


//MemberService가 IMemberMapper에 의존적이도록 만들기
@Service(value = "dev.xowoony.studymemberbbs.services.MemberService")
// @Service -> 스프링부트가 인식해야하는 서비스임을 알리는 어노테이션
public class MemberService {
    // MemberService랑 IMemberMapper를 서로 의존적이게 만든다.
    private final JavaMailSender mailSender;     //의존성 주입
    private final TemplateEngine templateEngine;    // 템플릿 엔진
    private final IMemberMapper memberMapper;


    // alt+insert 생성자로 가던가 alt+enter해서 제일 위에거 클릭 해주면 쭈르륵 나와요
    //IMemberMapper인터페이스 뒤에오는 membermapper는 내가 정하면 된다.
    @Autowired
    //의존성 주입을 위해 사용하는 어노테이션인 Autowired
    public MemberService(JavaMailSender mailSender, TemplateEngine templateEngine, IMemberMapper MemberMapper) {
        this.mailSender = mailSender;
        this.templateEngine = templateEngine;
        this.memberMapper = MemberMapper;
        // this. 자기생성자 호출. 어떤 참조타입이 객체화 되기 위해 반드시 한번 실행되어야 한다.
    }


    // 3개를 여기 메서드에 모두 적겠다.
    // 트랜잭션 적용하기 위해서
    @Transactional
    // Transactional은 3개중 하나라도 제대로 안되면 전부다 취소시키는 것을 구현
    // 누구한테 보낼지?
    // SendEmailAuthResult, CommonResult 두개를 받아와야함.
    // Enum<?> 는 타입 즉 제네릭을 구분하지 않으며, 아무 열거형이나 다 반환이 가능하다..
    // Enum을 적어주는 이유는 두개의 공통된 것을 적어줘야 받아올 수 있기 때문
    // 매개변수로 유저엔티티를 받는다 user에게 이메일인증을 전송하겠다.
    // 제한적으로 사용되므로 열거형을 써야한다.
    // ABCD : Enum<ABCD>
    // 열거형이라면 Enum
    public Enum<? extends IResult> sendEmailAuth(UserEntity user, EmailAuthEntity emailAuth)
            throws NoSuchAlgorithmException, MessagingException {
        // 뭔지 모르겠는데 IResult 인터페이스를 상속받거나 구현하고 있어야 한다라는 표현이다.
        // enum 인데 제네릭을 구분하지는 않겠다.
        // 매개변수로 UserEntity를 받는다

        UserEntity existingUser = this.memberMapper.selectUserByEmail(user.getEmail());
        // user가 가지고 있는 Email값을 넣는다. 여기 user는 위 user랑 같다. 위 user는 컨트롤러가 준다.
        if (existingUser != null) {
            // 이미 사용중인 이메일이라면 (값이 있다면)
            return SendEmailAuthResult.EMAIL_DUPLICATED;
            // 이자리에서 메서드 실행이 끝난다.
            // 이메일이 복사되었다.
        }
        String authCode = RandomStringUtils.randomNumeric(6);
        // 테스트코드로 해볼거에요. (test디렉토리-> RandomTest에서)
        // 랜덤한 숫자 문자열 6자리를 돌려줌
        // apache.commons 의존성 추가 후 작성
        String authSalt = String.format("%s%s%f%f",
                user.getEmail(),
                authCode,
                Math.random(),
                Math.random());
        StringBuilder authSaltHashBuilder = new StringBuilder();
        MessageDigest md = MessageDigest.getInstance("SHA-512");
        md.update(authSalt.getBytes(StandardCharsets.UTF_8));
        for (byte hashByte : md.digest()) {
            authSaltHashBuilder.append(String.format("%02x", hashByte));
        }
        authSalt = authSaltHashBuilder.toString();

        Date createdOn = new Date();
        Date expiresOn = DateUtils.addMinutes(createdOn, 5);

        emailAuth.setEmail(user.getEmail());
        emailAuth.setCode(authCode);
        emailAuth.setSalt(authSalt);
        emailAuth.setCreatedOn(createdOn);
        emailAuth.setExpiresOn(expiresOn);
        emailAuth.setExpired(false);

        if (this.memberMapper.insertEmailAuth(emailAuth) == 0) {
            return CommonResult.FAILURE;
        }

        // 여기부턴 간단한 텍스트 파일 보낼 때 (웬만하면 쓸일이 없겠음)
//        SimpleMailMessage mail = new SimpleMailMessage();
//        mail.setFrom("xowoony@gmail.com");
//        mail.setTo(user.getEmail());
//        mail.setSubject("[스터디] 회원가입 인증 번호"); // 제목
//        mail.setText(emailAuth.getCode()); // 인증번호
//        this.mailSender.send(mail);
        Context context = new Context();
        // 타임리프꺼를 써야함
        // 서비스에서 html로 인증코드를 넘겨줘야 하는데 그게 Context가 하는 일이다.
        context.setVariable("code", emailAuth.getCode());

        String text = this.templateEngine.process("member/registeremailAuth", context);
        // modelandview뭐시기 거기랑 똑같소
        MimeMessage mail = this.mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mail, "UTF-8");
        helper.setFrom("xowoony@gmail.com");
        helper.setTo(user.getEmail());
        helper.setSubject("[스터디] 회원가입 인증 번호");
        helper.setText(text, true);
        this.mailSender.send(mail);

        return CommonResult.SUCCESS;

    }

    @Transactional
    public Enum<? extends IResult> verifyEmailAuth(EmailAuthEntity emailAuth) {
        // emailAuth는 컨트롤러가 준것이다.
        EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByEmailCodeSalt(
                // existingEmailAuth 는 모든 값을 다 가지고 있다.
                emailAuth.getEmail(),
                emailAuth.getCode(),
                emailAuth.getSalt());
        if (existingEmailAuth == null) {
            // 3개 중 한 개 이상이 잘못되었을 경우 즉 인증번호가 잘못되었을 경우
            return CommonResult.FAILURE;
        }
        // 5분 지났을 경우(만료)
        if (existingEmailAuth.getExpiresOn().compareTo(new Date()) < 0) {
            // getExpiresOn(만료시점) 에서  new Date 를 빼줬다고 생각하라.
            return VerifyEmailAuthResult.EXPIRED;
        }


        //expired 올바른 인증번호를 입력하고 누르면 True가 되어야만 한다.
        existingEmailAuth.setExpired(true);
        if (this.memberMapper.updateEmailAuth(existingEmailAuth) == 0) {
            // 영향을 받은 레코드 갯수가 0일 경우
            return CommonResult.FAILURE;
        }
        return CommonResult.SUCCESS;
    }

    public Enum<? extends IResult> register(UserEntity user, EmailAuthEntity emailAuth) {
        // 1. 'emailAuth'가 가진 'email', 'code', 'salt'값 기준으로 새로운 'EmailAuthEntity' SELECT 해서 가져오기
        EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByEmailCodeSalt(
                // 값 세개를 넘겨받는다.
                emailAuth.getEmail(),
                emailAuth.getCode(),
                emailAuth.getSalt());
        // 2. <1>에서 가져온 새로운 객체가 null 이거나 이가 가진 isExpired() 호출 결과가 false 인 경우 'RegisterResult.EMAIL_NOT_VERIFIED'를 결과로 반환하기. (없음으로 만들어야 함).

        if (existingEmailAuth == null || !existingEmailAuth.isExpired()) {
            // 이메일인증을 정상적으로 완료하지 않았을 때
            // RegisterResult enums만들어 주고 와야함.
            return RegisterResult.EMAIL_NOT_VERIFIED;
            // 거절.
        }
        user.setPassword(CryptoUtils.hashSha512(user.getPassword()));
        // 비밀번호 암호화 (해싱)
        // CryptoUtils에 따로 빼놓은 것을 불러온다. 비밀번호가 해싱된 채로 반환 된 것을.
        //3. 'user'객체를 'IMemberMapper' 객체의 'insertUser' 메서드 호출시 전달인자로 하여 INSERT 하기.
        // 4. <3> 의 결과가 0이면, 'CommonResult.FAILURE' 반환하기.
        if (this.memberMapper.insertUser(user) == 0) {
            // 뭔지 모르겠지만 인서트가 안되었을 경우
            return CommonResult.FAILURE;
            // 실패
        }
        // 5. 위 과정 전체를 거친 후 'CommonResult.SUCCESS' 반환하기.
        return CommonResult.SUCCESS;
    }

    @Transactional
    // recoverPasswordSend 리펙터링 하라는데
    public Enum<? extends IResult> recoverPasswordSend(EmailAuthEntity emailAuth) throws MessagingException {
        UserEntity existingUser = this.memberMapper.selectUserByEmail(emailAuth.getEmail());
        // user가 가지고 있는 Email값을 넣는다. 여기 user는 위 user랑 같다. 위 user는 컨트롤러가 준다.
        if (existingUser == null) {
            // 이미 사용중인 이메일이라면 (값이 없다면)
            return CommonResult.FAILURE;
            // 이자리에서 메서드 실행이 끝난다.
            // 이메일이 복사되었다.
            // SUCCESS의 경우
        }
        String authCode = RandomStringUtils.randomNumeric(6);
        String authSalt = String.format("%s%s%f%f",
                authCode,
                emailAuth.getEmail(),
                Math.random(),
                Math.random());

        authSalt = CryptoUtils.hashSha512(authSalt);
        Date createOn = new Date(); // 현재일시
        Date expiresOn = DateUtils.addMinutes(createOn, 5); // 5분 미래
        emailAuth.setCode(authCode);
        emailAuth.setSalt(authSalt);
        emailAuth.setCreatedOn(createOn);
        emailAuth.setExpiresOn(expiresOn);
        emailAuth.setExpired(false);
        if (this.memberMapper.insertEmailAuth(emailAuth) == 0) {
            return CommonResult.FAILURE;
        }

        Context context = new Context();
        context.setVariable("email", emailAuth.getEmail());
        context.setVariable("code", emailAuth.getCode());
        context.setVariable("salt", emailAuth.getSalt());

        // 타임리프꺼를 써야함
        // 서비스에서 html로 인증코드를 넘겨줘야 하는데 그게 Context가 하는 일이다.
        context.setVariable("code", emailAuth.getCode());

        String text = this.templateEngine.process("member/recoverPasswordemailAuth", context);
        // modelandview뭐시기 거기랑 똑같소
        MimeMessage mail = this.mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mail, "UTF-8");
        helper.setFrom("xowoony@gmail.com");
        helper.setTo(emailAuth.getEmail());
        helper.setSubject("[스터디] 비밀번호 재설정 인증 링크");
        helper.setText(text, true);
        this.mailSender.send(mail);
        return CommonResult.SUCCESS;
        // 사용중인 이메일이 면 success.
    }

    public Enum<? extends IResult> recoverPasswordcheck(EmailAuthEntity emailAuth) {
        // 적기 전에 Membermapper.xml select 하고
        EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByIndex(emailAuth.getIndex());
        if (existingEmailAuth == null || !existingEmailAuth.isExpired()) {
            return CommonResult.FAILURE;
        }
        emailAuth.setCode(existingEmailAuth.getCode());
        emailAuth.setSalt(existingEmailAuth.getSalt());
        return CommonResult.SUCCESS;
    }


    @Transactional
    public Enum<? extends IResult> recoverPasswordAuth(EmailAuthEntity emailAuth) {
        EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByEmailCodeSalt(
                emailAuth.getEmail(),
                emailAuth.getCode(),
                emailAuth.getSalt());
        if (existingEmailAuth == null) {
            return CommonResult.FAILURE;
        }
        if (new Date().compareTo(existingEmailAuth.getExpiresOn()) > 0) {
            return CommonResult.FAILURE;
        }
        existingEmailAuth.setExpired(true);
        if (this.memberMapper.updateEmailAuth(existingEmailAuth) == 0) {
            return CommonResult.FAILURE;
        }
        return CommonResult.SUCCESS;
    }


    // 비밀번호 재설정     // 1109 과제
    @Transactional
    public Enum<? extends IResult> recoverPassword(EmailAuthEntity emailAuth, UserEntity user) {
        //
        EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByEmailCodeSalt(
                emailAuth.getEmail(),
                emailAuth.getCode(),
                emailAuth.getSalt());
        if (existingEmailAuth == null || !existingEmailAuth.isExpired()) {
            return CommonResult.FAILURE;
        }
        // 이메일 인증이 아직 안됐거나 인증 시간이 5분이 지나서 만료되었을 경우 실패를 반환한다.
        UserEntity existingUser = this.memberMapper.selectUserByEmail(existingEmailAuth.getEmail());
        // 이메일값을 기준으로 DB에서 셀렉트 해온 값.existingUser는 DB에서 왔다.
        existingUser.setPassword(CryptoUtils.hashSha512(user.getPassword()));
        // 새롭게 입력한 비밀번호로 해싱 후 비밀번호를 수정한다.
        if (this.memberMapper.updateUser(existingUser) == 0) {
            // 데이터를 싹다 가지고 와서 그중에 비밀번호만 바꾸고 다시 집어넣는 과정이다.
            return CommonResult.FAILURE;
        }
        return CommonResult.SUCCESS;
    }

    // 로그인
    @Transactional
    public Enum<? extends IResult> login(UserEntity user) {
        UserEntity existingUser = this.memberMapper.selectUserByEmailPassword(
                user.getEmail(),
                CryptoUtils.hashSha512(user.getPassword()));
        if (existingUser == null) {
            System.out.println("실패!!!!");
            return CommonResult.FAILURE;
        }
        System.out.println("성공!!!!");
        return CommonResult.SUCCESS;
    }

    // 이메일 찾기
    @Transactional
    public Enum<? extends IResult> recoverEmail(UserEntity user) {
        UserEntity findEmail = this.memberMapper.selectUserByNameContact(
                user.getName(),
                user.getContact());
        if (findEmail == null) {
            return CommonResult.FAILURE;
        }
        return CommonResult.SUCCESS;
    }

}

MemberMapper.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="dev.xowoony.studymemberbbs.mappers.IMemberMapper">
    <!--인서트-->
    <insert id="insertEmailAuth"
            useGeneratedKeys="true"
            keyColumn="index"
            keyProperty="index"
            parameterType="dev.xowoony.studymemberbbs.entities.member.EmailAuthEntity">
        INSERT INTO `study_member`.`email_auths` (`email`, `code`, `salt`, `created_on`, `expires_on`, `expired_flag`)
        VALUES (#{email}, #{code}, #{salt}, #{createdOn}, #{expiresOn}, #{isExpired})
    </insert>

    <insert id="insertUser"
            keyColumn="email"
            keyProperty="email"
            parameterType="dev.xowoony.studymemberbbs.entities.member.UserEntity">

        INSERT INTO `study_member`.`users` (`email`, `password`, `nickname`, `name`, `contact`, `address_postal`,
                                            `address_primary`, `address_secondary`)
        VALUES (#{email}, #{password}, #{nickname}, #{name}, #{contact}, #{addressPostal}, #{addressPrimary},
                #{addressSecondary})
    </insert>


    <select id="selectEmailAuthByEmailCodeSalt"
            resultType="dev.xowoony.studymemberbbs.entities.member.EmailAuthEntity">
        SELECT `index`        AS `index`,
               `email`        AS `email`,
               `code`         AS `code`,
               `salt`         AS `salt`,
               `created_on`   AS `createdOn`,
               `expires_on`   AS `expiresOn`,
               `expired_flag` AS `isExpired`
        FROM `study_member`.`email_auths`
        WHERE BINARY `email` = #{email}
          AND BINARY `code` = #{code}
          AND BINARY `salt` = #{salt}
            /*BINARY는 웬만하면 하세요 안해도 되지만*/
        LIMIT 1
    </select>


    <select id="selectEmailAuthByIndex"
            resultType="dev.xowoony.studymemberbbs.entities.member.EmailAuthEntity">
        SELECT `index`        AS `index`,
               `email`        AS `email`,
               `code`         AS `code`,
               `salt`         AS `salt`,
               `created_on`   AS `createdOn`,
               `expires_on`   AS `expiresOn`,
               `expired_flag` AS `isExpired`
        FROM `study_member`.`email_auths`
        WHERE BINARY `index` = #{index}
    </select>

    <select id="selectUserByEmailPassword"
            resultType="dev.xowoony.studymemberbbs.entities.member.UserEntity">
        SELECT `email`             AS `email`,
               `password`          AS `password`,
               `nickname`          AS `nickname`,
               `name`              AS `name`,
               `contact`           AS `contact`,
               `address_postal`    AS `addressPostal`,
               `address_primary`   AS `addressPrimary`,
               `address_secondary` AS `addressSecondary`,
               `registered_on`     AS `registeredOn`
        FROM `study_member`.`users`
        WHERE BINARY `email` = #{email}
          AND BINARY `password` = #{password}
        LIMIT 1
    </select>


    <select id="selectUserByEmail"
            resultType="dev.xowoony.studymemberbbs.entities.member.UserEntity">
        /*id를 selectUserByEmail 로 준다.*/
        /*select는 resultType이 있어야 한다.*/
        /*별명은 UserEntity에 setEmail 이런식으로 있어야 하오*/
        SELECT `email`             AS `email`,
               `password`          AS `password`,
               `nickname`          AS `nickname`,
               `name`              AS `name`,
               `contact`           AS `contact`,
               `address_postal`    AS `addressPostal`,
               `address_primary`   AS `addressPrimary`,
               `address_secondary` AS `addressSecondary`,
               `registered_on`     AS `registeredOn`
        FROM `study_member`.`users`
        WHERE BINARY `email` = #{email}
        LIMIT 1
    </select>


    <update id="updateEmailAuth"
            parameterType="dev.xowoony.studymemberbbs.entities.member.EmailAuthEntity">
        UPDATE `study_member`.`email_auths`
        SET `email`        = #{email},
            `code`         = #{code},
            `salt`         = #{salt},
            `created_on`   = #{createdOn},
            `expires_on`   = #{expiresOn},
            `expired_flag` = #{isExpired}
        WHERE BINARY `index` = #{index}
        LIMIT 1
    </update>


    <update id="updateUser"
            parameterType="dev.xowoony.studymemberbbs.entities.member.UserEntity">
        UPDATE `study_member`.`users`
        SET `password`          = #{password},
            `nickname`          = #{nickname},
            `name`              = #{name},
            `contact`= #{contact},
            `address_postal`    = #{addressPostal},
            `address_secondary` = #{addressSecondary},
            `registered_on`     = #{registeredOn}
        WHERE BINARY `email` = #{email}
        LIMIT 1
    </update>

    <select id="selectUserByNameContact"
            resultType="dev.xowoony.studymemberbbs.entities.member.UserEntity">
        SELECT `email`             AS `email`,
               `password`          AS `password`,
               `nickname`          AS `nickname`,
               `name`              AS `name`,
               `contact`           AS `contact`,
               `address_postal`    AS `addressPostal`,
               `address_primary`   AS `addressPrimary`,
               `address_secondary` AS `addressSecondary`,
               `registered_on`     AS `registeredOn`
        FROM `study_member`.`users`
        WHERE BINARY `name` = #{name}
          AND BINARY `contact` = #{contact}
        LIMIT 1
    </select>


</mapper>

IMemberMapper.java (인터페이스)

package dev.xowoony.studymemberbbs.mappers;

import dev.xowoony.studymemberbbs.entities.member.EmailAuthEntity;
import dev.xowoony.studymemberbbs.entities.member.UserEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

// mybatis-spring-boot-starter 의존성을 주입시켰기 때문에 인식이 가능하다.
@Mapper
// 맵핑
public interface IMemberMapper {
    int insertEmailAuth(EmailAuthEntity emailAuthEntity);

    // param 어노테이션, @Param은 여러개를 불러올 수 있다.
    EmailAuthEntity selectEmailAuthByEmailCodeSalt(@Param(value = "email") String email,
                                                   @Param(value = "code") String code,
                                                   @Param(value = "salt") String salt);

    EmailAuthEntity selectEmailAuthByIndex(@Param(value = "index") int index);

    UserEntity selectUserByEmail(@Param(value = "email") String email);

    UserEntity selectUserByEmailPassword(@Param(value = "email") String email,
                                         @Param(value = "password") String password);


    int updateEmailAuth(EmailAuthEntity emailAuth);
    // int타입을 반환하는 updateEmailAuth => 영향을 받은 갯수.

    int insertUser(UserEntity user);

    int updateUser(UserEntity user);
    // updateUser 라고 만들어놓으면  password 등등 많이 쓰일일이 있으므로 편함


    // 이메일 인증 구현
    UserEntity selectUserByNameContact(@Param(value = "name") String name,
                                       @Param(value = "contact") String contact);
}

'Spring Boot > 구현해보기' 카테고리의 다른 글

게시판 글쓰기 구현 + DB에 인서트 하기  (0) 2022.11.17
게시판 글쓰기 구현  (0) 2022.11.14
비밀번호 재설정 구현2  (0) 2022.11.13
비밀번호 재설정 구현  (0) 2022.11.13
로그인 페이지 구현  (0) 2022.11.12
Comments