관리 메뉴

JIE0025

[SMTP] 이메일 구현과 관련 이슈 기록 본문

백엔드/이슈, 트러블슈팅

[SMTP] 이메일 구현과 관련 이슈 기록

Kangjieun11 2023. 4. 7. 23:24
728x90

 

✅  이메일 전송 구현

 
이메일 전송에 사용가능한 서비스는 여러가지가 있다.
대표적으로는 AWS Email Service도 존재하지만 이번 마이버디 프로젝트에서는 Spring boot 내부의 SMTP를 도입하였다. 
 
 

✅  Why SMTP ? 

왜 Spring boot의 SMTP를 사용했는가?
내부 SMTP는 애플리케이션에서 이메일을 보낼 수 있는 가장 간단한 방법이다.
 
Spring boot에서는 JavaMailSender인터페이스가 존재한다. 
이 인터페이스를 통해 이메일 전송에 기본적인 설정이 되어 있고, SMTP 서버를 통해 이메일이 전송된다. 
 
 
SMTP의 장점
1. 안정성 : 이메일 전송을 위한 표준 프로토콜이라서 안정성이 높다. 
2. 보안 : 이메일 전송에 TLS/SSL과 같은암호화 기술을 적용이 가능, 데이터의 안전성이 보장된다. 
3. 호환성 : 다양한 OS, 프로그래밍 언어에서 지원되어 쉽게 사용 가능
4. 유연 : 서버/클라이언트 간 상호작용 가능한 다양한 명령어를 제공
 
 
AWS Email Service는 메일 서버 구축, 설정 등에 필요한 작업이 많이 줄어들기 때문에 구축 및 운영이 쉽다고 하지만,,
전송 속도가 SMTP보다 느리며, 보안 등의 추가 작업이 필요할 수도 있다고 한다.
또 추가 비용이 들어갈 수도 있을것이다.
 
 
Spring Boot의 SMTP는 사용하기에도 편한데, Gmail의 경우엔 하루에 500건까지 무료로 사용가능하다고 하니 비용면에서도 합리적이다!
 
SMTP를 도입을 안할 이유가 없었다
 
 

✅  이메일 전송 Setting

 
나는 서비스의 공식계정으로 사용하기 위해 Gmail을 생성해주었다.
 
 
build.gradle

implementation 'org.springframework.boot:spring-boot-starter-mail'

 
application.yml

spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: 이메일 전송에 사용될 계정
    password: 이메일 전송에 사용될 계정의 비밀번호
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true

 
 
이렇게 설정했더니 Exception이 발생했다.

Spring Mail AuthenticationFailedException

 

Exception in thread "main" org.springframework.mail.MailAuthenticationException: Authentication failed; nested exception is javax.mail.AuthenticationFailedException: 535-5.7.8 Username and Password not accepted. Learn more at
535 5.7.8  https://support.google.com/mail/?p=BadCredentials o84sm6724751pfi.172 - gsmtp

	at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:439)
	at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:322)
	at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:311)
	at me.devson.email.EmailApplication.main(EmailApplication.java:27)
Caused by: javax.mail.AuthenticationFailedException: 535-5.7.8 Username and Password not accepted. Learn more at
535 5.7.8  https://support.google.com/mail/?p=BadCredentials o84sm6724751pfi.172 - gsmtp

	at com.sun.mail.smtp.SMTPTransport$Authenticator.authenticate(SMTPTransport.java:965)
	at com.sun.mail.smtp.SMTPTransport.authenticate(SMTPTransport.java:876)
	at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:780)
	at javax.mail.Service.connect(Service.java:366)
	at org.springframework.mail.javamail.JavaMailSenderImpl.connectTransport(JavaMailSenderImpl.java:517)
	at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:436)
	... 3 more

 
 stackoverflow에서도 보안 수준이 낮은 앱의 액세스를 허용 설정하라는 많은 이야기가 존재했지만,
이는 안전하지 못할 수 있다는 한 블로그를 참고했다. 
 
 
1. 로그에 찍힌 링크  https://support.google.com/mail/?p=BadCredentials 를 접속
2. 2단계의 2번째에 앱 비밀번호 사용이 존재하는데, 앱 비밀번호를 클릭한다.

3. 앱 비밀번호 생성 방법에서
1. Google 계정으로 이동합니다  -> Google계정 클릭
 
4. 계정 화면에서 좌측 탭에 존재하는  <보안>을 클릭한다.

 
5.  2단계 인증 설정을 한다.
6. 다시 계정 화면으로 돌아오면 앱비밀번호를 설정할 수 있다. 앱 비밀번호를 클릭한 후 재인증을 한다.
7. 메일 -> 사용할 컴퓨터를 설정하고, 생성을 누르면 앱비밀번호가 생성된다. 
 
 
이 앱 비밀번호를 application.yml 설정에 추가한다.  (로컬에서는 static으로 넣어도 되지만, 이후를 위해 환경변수 설정은 해야한다)

spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: 이메일 전송에 사용될 계정
    password: ----앱비밀번호----
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true

 
 
이후 관련 코드를 적어주면된다.
 
 

✅  이메일 전송 서비스 구현

 
 html을 이용해 간단한 이메일 전송테스트를 해볼 수 있다. 
 
아래는 비밀번호 재설정을 위한 이메일 전송 로직이다.

@Service
@RequiredArgsConstructor
public class EmailServiceImpl implements EmailService{

    @Autowired
    private JavaMailSender mailSender;
    
    private static final String DOMAIN = "도메인 주소 입력";

    @Value("${spring.mail.username}")
    private String fromEmail;


    @Override
    public void sendEmailForPasswordReset(String toEmail) throws MessagingException {

        Member member = verifiedEmailMember(toEmail);

        String token = jwtTokenizer.delegateTokenForNewPassword(member);
        String resetLink = DOMAIN + "/password/reset?token=" + token;

        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true);

        helper.setFrom(fromEmail);
        helper.setTo(toEmail);

        String emailText = "비밀번호 재설정 버튼을 클릭해 새로운 비밀번호를 설정할 수 있습니다.";
        String buttonText = "비밀번호 재설정";
        String html = setEmailHtmlWithButton(emailText, resetLink, buttonText); //프론트 링크와 토큰
        helper.setSubject("[마이버디] 비밀번호 재설정 링크를 보내드립니다.");
        helper.setText(html,true);
        mailSender.send(message);
    }

    private String setEmailHtmlWithButton(String emailText, String link, String buttonText) {
        return "<html><head><style>" +
                "body { font-family: Arial, sans-serif; font-size: 14px; }" +
                "a,a:visited { text-decoration: none; color: #00AE68;}" +
                "a.button { display: block; position: relative; float: left; width: 120px; padding: 0; margin: 10px 10px 10px 0; font-weight: 600; text-align: center; line-height: 50px; color: #FFF; border-radius: 5px; transition: all 0.2s; }" +
                ".realButton { background: #323F4F; }" +
                ".realButton.btnPush { box-shadow: 0px 5px 0px 0px #F4B870; }" +
                ".btnPush:hover { margin-top: 15px; margin-bottom: 5px; }" +
                ".realButton.btnPush:hover { box-shadow: 0px 0px 0px 0px #F4B870; }" +
                ".clear { clear: both; }" +
                "</style></head><body>" +
                "<p>" + emailText + "</p>" +
                "<div class=\"container\">" +
                "<a href=\"" + link + "\" title=\"Button fade blue/green\" class=\"button btnPush realButton\">"+buttonText+"</a>" +
                "<div class=\"clear\"></div>" +
                "</div></body></html>";
    }
}

 
 
 

✅ 이메일 전송시 걸리는 시간은..

 
이메일 전송은 시간이 오래 걸리는 작업이다. 

 
.sendEmailForNewPassword(EmailDto) executed in 5198ms-------—

 

 

SMTP 자체가 일반적으로 전송 시간이 많이 걸린다고 하며,
이를 내가 개선할 방법은 극히 제한적이다.
 
 

✅ 사용자 경험 개선의 필요성

현재 유저는 응답을 받을 때까지, 메일이 전송되었는지 확인할 수 없는데, 이는 사용자 경험에 치명적일 수 있다.
(실제로 친오빠한테 테스트를 부탁했는데, 이메일 전송이 되지 않은걸로 오해하여 전송버튼을 2번이나 눌렀다.)
 
내가 SMTP 서버의 성능을 확장 시키는 것보다는 SMTP 전송을 비동기적으로 처리하는 것이 사용자 경험에는 더욱 좋을것으로 생각한다. 
 
1) 이메일 전송 요청을 보내고,
2) 사용자는 바로 이메일이 전송되었다는 메세지를 볼 수 있게 하면..
 
전송시간에 구애받지 않고, 사용자는 서비스를 이용할 수 있게 될것이다.
 
 
 

✅ 비밀번호 변경 이메일 관련 이슈

현재 비밀번호 변경과 관련된 토큰의 유효기간은 24시간이다.
 
토큰은 자유이용권과 같은 개념이라, 계속 이용할 수 있고, 이는 토큰이 탈취될 경우 아주 보안에 취약하다.
현재 토큰을 URL과
이는 보안상 토큰이 탈취될 경우 좋지 못할것을 예상할 수 있다. 
 
토큰은 이메일을 받은 유저가 토큰의 내용을 볼 수 있게 설정되어있다. 

 

String resetLink = DOMAIN + "/password/reset?token=" + token;

 
이는 누구나 BASE64로 디코딩을 할 수 있기 때문에,
이메일 받은 계정에 대해 로그인을 해놓고 로그아웃을 까먹은 경우 -> 악의적인 사용자가  해당 이메일로 비밀번호를 변경할 수도 있는것이다. 
 
 
따라서 24시간이 아니라 10분정도로 토큰의 시간을 줄이는게 안전할 것을 예상한다. 
 
토큰의 유효시간을 짧게 설정하면, 24시간이나 10분이나 사용자 경험 차이가 없겠지만 보안성은 훨씬 향상될 수 있다.
 
10분 이후에는 토큰이 만료되어 새로운 토큰 발급을 요청해야 하므로 피해를 최소화할 수 있을것이다.
 
 
 
 
 
 


 
출처

https://ivvve.github.io/2019/02/09/java/Spring/mail_AuthenticationFailedException/