[스프링부트+JPA+타임리프] SMTP Gmail 이용하여 이메일 전송 구현 (임시 비밀번호 발송 기능)

jyleever·2022년 5월 24일
3

비밀번호를 분실한 회원에게 임시 비밀번호를 전송하여 임시 비밀번호로 로그인할 수 있는 기능을 추가했다.
Gmail SMTP 프로토콜을 이용하여 이메일을 전송하는 기능을 구현했다.

개발 환경

  • Spring boot 2.6.7
  • Gradle

개발 순서

  1. build.gradle에 spring-boot-starter-mail 의존성을 추가한다.
  2. 메일을 처리하는 Gmail 계정을 생성하고 SMTP 설정을 확인하여 application.yml 파일을 작성한다.
  3. 임시 비밀번호를 발급하고 메일을 처리하기 위해 MailVo, MailService, MemberController, MemberService 을 통해 메일을 전송한다.

SMTP 프로토콜

  • Simple Mail Transfer Protocol의 약어
  • 인터넷상에서 이메일을 전송하기 위해서 사용되는 통신 규약 중에 하나
  • SMTP서버 : 이메일을 송수신하는 서버
  • SMTP서버를 구축하기 위해서는 물리적인 서버(리눅스 등)를 구축하여 서버를 설치하고 네트워크 환경을 잡아줘야 하지만, 네이버와 구글에서 계정에 대한 SMTP를 제공해주고 있으므로 해당 방법을 사용할 것

Gmail SMTP

구글 계정만 있으면 무료로 발송할 수 있는 Gmail SMTP Server

Gmail SMTP 기본 설정

  • SMTP 서버 : smtp.gmail.com
  • SMTP 이메일 : 본인 Gmail 주소 
  • SMTP 비밀번호 : 본인 Gmail 비밀번호
  • SMTP 포트 (TLS) : 587
  • SMTP 포트 (SSL) : 465
  • SMTP TLS/SSL 필요 : 예

Gmail SMTP 설정 순서

  1. https://myaccount.google.com/u/2/security 링크로 접속하여 2단계 인증을 사용하도록 한다(휴대폰 인증 등을 거침)

  2. 앱 비밀번호 생성

  • 앱 선택 : 메일
  • 기기 선택 : 기타(맞춤 이름)


  • 앱 이름 입력을 입력하면 앱 비밀번호가 자동으로 생성된다. 이 비밀번호를 application.yml 설정파일에 사용한다.

스프링부트 설정

  • application.yml
spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: helpringproject@gmail.com
    password: ...
    properties:
      mail.smtp.auth: true
      mail.smtp.starttls.enable: true

구글은 인증에 TLS를 사용한다.
TLS는 SSL과 비슷한 것인데, SSL 3.0 부터 TLS 라고 부르며, 이는 통신에 사용되는 데이터를 암호화 하는 것이다.

  • build.gradle
dependencies {
	...
	implementation 'org.springframework.boot:spring-boot-starter-mail'

spring-boot-starter-mail 의존성을 추가한다.
스프링부트가 자동으로 JavaMailSender을 빈으로 등록해준다.

코드

  • 비밀번호 찾기 과정
  1. 로그인 화면에서 비밀번호 찾기를 누르면 모달창으로 비밀번호 창이 뜬다.
  2. 사용자가 이메일을 입력하면 서버는 DB에 존재하는 이메일인지 확인한다.
  3. DB에 존재하는 이메일이면 임시 비밀번호를 생성하여 DB에 저장한다.
  4. 스프링 프레임워크의 JavaMailSender 인터페이스를 이용하여 해당 이메일로 임시 비밀번호를 포함한 메일을 전송한다.

post-login.html

<!-- 비밀번호 찾기 모달 -->
<th:block th:replace="/layout/fragment/modal::findPasswordFragment"></th:block>
...
<button type="button" class="btn btn-link" data-bs-toggle="modal"
data-bs-target="#findPw">비밀번호를 잊으셨나요?</button>
...
  • findPasswordFragment

<!--임시 비밀번호 모달-->
<th:block th:fragment="findPasswordFragment">
    <!-- 비밀번호 찾기 모달 -->
    <div id="findPw" class="modal fade">
    	...
    	<div class="card-body">
    		<div class="text-start">
    		<p>입력한 이메일로 임시 비밀번호가 전송됩니다.</p>
    			<div class="input-group input-group-outline my-3">
    				<label class="form-label">Email</label>
    					<input type="email" id="memberEmail" name="memberEmail" class="form-control" required>
    			</div>
    			<div class="text-center">
    				<button type="button" class="btn bg-gradient-primary w-100 my-4 mb-2"
    					id="checkEmail">비밀번호 발송</button>
				...
</th:block>

  • ajax
<script>

    const header = $("meta[name='_csrf_header']").attr('content');
    const token = $("meta[name='_csrf']").attr('content');

    $('#checkEmail').on('click', function(){
        checkEmail();
    });

    function checkEmail(){
        const memberEmail = $('#memberEmail').val();

        if(!memberEmail || memberEmail.trim() === ""){
            alert("이메일을 입력하세요.");
        } else {
            $.ajax({
                type: 'GET',
                url: '/rest/checkEmail',
                data: {
                    'memberEmail': memberEmail
                },
                dataType: "text",
                beforeSend: function (xhr) {
                    xhr.setRequestHeader(header, token);
                }
            }).done(function(result){
                console.log("result :" + result);
                if (result == "true") {
                    sendEmail();
                    alert('임시비밀번호를 전송 했습니다.');
                    window.location.href="/auth/login";
                } else if (result == "false") {
                    alert('가입되지 않은 이메일입니다.');
                }
            }).fail(function(error){
                alert(JSON.stringify(error));
            })
        }
    };

    function sendEmail(){
        const memberEmail = $('#memberEmail').val();

        $.ajax({
            type: 'POST',
            url: '/sendPwd',
            data: {
                'memberEmail' : memberEmail
            },
            beforeSend: function(xhr){
                xhr.setRequestHeader(header, token);
            },
            error: function(error){
                alert(JSON.stringify(error));
            }
        })
    }

</script>
  • /rest/checkEmail 컨트롤러에 GET 방식으로 memberEmail을 넘겨주면 해당 메일이 DB에 존재하는지 존재하지 않는지 result를 반환한다.
  • result 값에 따라 임시 비밀번호를 전송하는 컨트롤러를 호출하거나 가입되지 않은 이메일임을 알린다.

MemberController

뷰에서 memberEmail 을 파라미터로 받아 이메일이 존재하는지 확인하는 memberService 호출

    /** 이메일이 DB에 존재하는지 확인 **/
    @GetMapping("/checkEmail")
    public boolean checkEmail(@RequestParam("memberEmail") String memberEmail){

        log.info("checkEmail 진입");
        return memberService.checkEmail(memberEmail);
    }

MemberService

DB에 이메일이 존재하는지 확인

    /** 이메일이 존재하는지 확인 **/
    @Override
    public boolean checkEmail(String memberEmail) {

        /* 이메일이 존재하면 true, 이메일이 없으면 false  */
        return memberRepository.existsByEmail(memberEmail);
    }
  • 스프링 데이터 JPA를 이용해 메일이 존재하는지 확인하여 boolean 값을 반환한다.

MemberController

임시 비밀번호를 생성하고 메일을 생성 & 전송하는 컨트롤러

    /** 비밀번호 찾기 - 임시 비밀번호 발급 **/

    @PostMapping("/sendPwd")
    public String sendPwdEmail(@RequestParam("memberEmail") String memberEmail) {

        log.info("sendPwdEmail 진입");
        log.info("이메일 : "+ memberEmail);

        /** 임시 비밀번호 생성 **/
        String tmpPassword = memberService.getTmpPassword();

        /** 임시 비밀번호 저장 **/
        memberService.updatePassword(tmpPassword, memberEmail);

        /** 메일 생성 & 전송 **/
        MailVo mail = mailService.createMail(tmpPassword, memberEmail);
        mailService.sendMail(mail);

        log.info("임시 비밀번호 전송 완료");

        return "member/member-login";
    }
  • 임시 비밀번호를 생성하고 DB에 임시 비밀번호를 암호화하여 저장한다.
  • 메일 내용을 생성한 후 전송한다.

MemberService - 임시 비밀번호

임시 비밀번호를 암호화하고, member 객체의 updatePassword 메소드를 호출하여 임시 비밀번호 업데이트

    /** 임시 비밀번호 생성 **/
    @Override
    public String getTmpPassword() {
        char[] charSet = new char[]{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
                'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};

        String pwd = "";

        /* 문자 배열 길이의 값을 랜덤으로 10개를 뽑아 조합 */
        int idx = 0;
        for(int i = 0; i < 10; i++){
            idx = (int) (charSet.length * Math.random());
            pwd += charSet[idx];
        }

        log.info("임시 비밀번호 생성");

        return pwd;
    }

    /** 임시 비밀번호로 업데이트 **/
    @Override
    public void updatePassword(String tmpPassword, String memberEmail) {

        String encryptPassword = encoder.encode(tmpPassword);
        Member member = memberRepository.findByEmail(memberEmail).orElseThrow(() ->
                new IllegalArgumentException("해당 사용자가 존재하지 않습니다."));

        member.updatePassword(encryptPassword);
        log.info("임시 비밀번호 업데이트");
    }

Member

DB의 데이터 값을 변경하는 메소드이므로 해당 도메인 클래스에 메소드를 정의했다

    /** 비밀번호 변경 메서드 **/
    public void updatePassword(String password){
        this.password = password;
    }

MailService

메일 전송 처리 담당 서비스

@Service
@Transactional
@RequiredArgsConstructor
@Slf4j
public class MailServiceImpl implements MailService {

    private final JavaMailSender mailSender;
    
    private static final String title = "Helpring 임시 비밀번호 안내 이메일입니다.";
    private static final String message = "안녕하세요. Helpring 임시 비밀번호 안내 메일입니다. "
            +"\n" + "회원님의 임시 비밀번호는 아래와 같습니다. 로그인 후 반드시 비밀번호를 변경해주세요."+"\n";
    private static final String fromAddress = "helpringproject@gmail.com";

    /** 이메일 생성 **/
    @Override
    public MailVo createMail(String tmpPassword, String memberEmail) {

        MailVo mailVo = MailVo.builder()
                .toAddress(memberEmail)
                .title(title)
                .message(message + tmpPassword)
                .fromAddress(fromAddress)
                .build();

        log.info("메일 생성 완료");
        return mailVo;
    }

    /** 이메일 전송 **/
    @Override
    public void sendMail(MailVo mailVo) {
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setTo(mailVo.getToAddress());
        mailMessage.setSubject(mailVo.getTitle());
        mailMessage.setText(mailVo.getMessage());
        mailMessage.setFrom(mailVo.getFromAddress());
        mailMessage.setReplyTo(mailVo.getFromAddress());

        mailSender.send(mailMessage);

        log.info("메일 전송 완료");
    }
}


JavaMailSender

  • 스프링의 기본 메일 인터페이스는 MainSender이다. 이 인터페이스는 SimpleMailMessage 객체를 사용하고, 첨부파일 등을 지원하지 않는다.
  • JavaMailSender 인터페이스는 MailSender 인터페이스를 상속받았으며, 멀티 파트 데이터를 처리할 수 있는 MainSenderMIME 를 지원한다.
  • 현재 코드에서는 메세지 객체는 텍스트 데이터만을 전송하는 SimpleMailMessage를 이용했다. (확장성을 위해 JavaMailSender 인터페이스를 이용했다.)

SimpleMailMessage

SimpleMailMessage

  • 이메일 메시지 정보를 담은 객체
  • setTo() : 받는 사람 주소
  • setFrom() : 보내는 사람 주소
  • setSubject() : 제목
  • setText() : 메시지 내용
  • setReplyTo() : 답장 받을 메일 주소

mailSender.send : 실제 메일 발송

MailVo

/** 메일 메시지 정보 **/

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MailVo {
    private String toAddress; // 받는 이메일 주소
    private String title; // 이메일 제목
    private String message; // 이메일 내용
    private String fromAddress; // 보내는 이메일 주소

}
  • MailVo 객체를 따로 생성하여 보낼 이메일 주소, 제목, 내용, 임시 비밀번호, 받을 이메일 주소를 입력한다.
  • SimpleMailMessage 객체에 mailVo의 이메일 주소, 제목 등을 넘겨서 메일 정보를 설정해준다.

주의

  • 대부분 에러는 SMTP 설정을 잘못 작성하거나 잘못된 앱 비밀번호를 입력했을 때 발생한다
  • application.yml 파일에 메일을 보내는 관리자 메일주소 및 비밀번호 설정을 작성한 경우 github 등 공개된 곳에 올릴 때 반드시 해당 부분/파일을 .gitingore를 해줘야 한다.
  • 일일 사용량이 정해져 있기 때문에 실제 서비스를 할 땐 다음 사이트를 참조
    https://sendgrid.com/
    https://www.mailgun.com/
    https://aws.amazon.com/ses/
    https://gsuite.google.com/

에러 로그

출처
https://victorydntmd.tistory.com/342
https://www.siteground.com/kb/gmail-smtp-server/
https://intrepidgeeks.com/tutorial/spring-email-temporary-password-issuance
https://victorydntmd.tistory.com/342
https://offbyone.tistory.com/167
https://bamdule.tistory.com/238
https://velog.io/@max9106/Spring-Boot-Gmail-SMTP-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0%EB%A9%94%EC%9D%BC%EB%B3%B4%EB%82%B4%EA%B8%B0
https://ktko.tistory.com/entry/JAVA-SMTP%EC%99%80-Mail-%EB%B0%9C%EC%86%A1%ED%95%98%EA%B8%B0Google-Naver
https://privatenote.tistory.com/172

0개의 댓글