easycode

[Spring] SMTP로 html template(image 포함) 포함한 인증 메일 보내기(JAVA)(이메일 API) 본문

Spring

[Spring] SMTP로 html template(image 포함) 포함한 인증 메일 보내기(JAVA)(이메일 API)

ez() 2023. 11. 6. 02:39

이번 게시글의 완성본(예시)

현재 진행중인 프로젝트에서 초기 유저 생성 당시, 이메일 인증을 하는 로직이 있어 사용하게 되었습니다. 

 

사용스택 : SpringBoot(2.7.x), JAVA(JDK 11), JPA, Thymleaf, SMTP는 구글의 gmail을 사용하였습니다.

 

 


시작에 앞서, 제 이메일 인증 로직은 다음과 같습니다.

1. 프론트엔드에서 API 명세서에 적힌 uri를 통해 이메일 전송을 요청합니다. RequestBody(요청Body)엔 이메일 인증이 필요한 email이 들어갑니다(EmailRequestDto, Http 메소드는 POST)

2. 인증토큰 만료 시간이 지난 데이터들을 삭제합니다.

3. 프론트엔드에서 전송한 RequestBody에 담긴 이메일이 존재하는 지 중복체크합니다.

    3-1. 만약 중복된다면 responseBody(응답Body)의 statusName을 duplicated로 반환하며 중복됐다는 걸 알려줍니다.

4. 중복되지 않는다면, 해당 이메일을 받는사람으로 설정하고 만료 시간이 정해진 인증 토큰 값을 발행합니다. 인증하기 버튼에 인증을 실행할 api+이메일과 인증토큰 값을 설정합니다.

5. 이메일을 전송합니다.

    5-1. 전송에 성공했다면 responseBody의 statusName을 success로 반환합니다(EmailResponseDto)

    5-2. 전송에 실패했다면 responseBody의 statusName을 fail로 반환합니다(EmailResponseDto)

 

------- 아래는 생략 가능한 절차입니다 -------

 

6. 이메일 인증 테이블(EmailAuth)에 이메일과 인증토큰 값, 만료 시간 정보를 저장합니다.

6. 사용자가 메일의 인증하기 버튼을 누릅니다.

7. 인증하기 버튼에 담긴 이메일과 인증토큰 값을 받아 EmailAuth 테이블에서 조회합니다(EmailAuthRequestDto)

    7-1. 해당 데이터가 일치한다면 인증여부(certification)를 true로 설정한 후, responseBody의 emailAuth를 success로 반환합니다(EmailAuthResponseDto)

    7-2. 해당 데이터가 일치하지 않는다면 responseBody의  emailAuth를 fail로 반환합니다(EmailAuthResponseDto)

 

 

 


들어가기에 앞서 SMTP는 뭔가요?

Simple Mail Transfer Protocol의 약자로, 인터넷을 통해 이메일 메시지를 보내고 받는 데 사용되는 통신 프로토콜입니다.

 


 

1. Gmail 앱 비밀번호 생성

먼저 이메일을 보낼 계정의 앱 비밀번호를 생성해야 합니다. 저 같은 경우엔 해당 프로젝트에서만 쓰일 계정을 따로 만들어주었으나, 원래 본인 계정으로 진행해도 됩니다.

앱 비밀번호를 생성하기 위해선 2단계 인증이 활성화되어 있어야 합니다. (이미 활성화되어 있다면 생략해주세요)

Google 홈에서 우측 상단 내 프로필 클릭 - Google 계정관리 - 보안으로 이동하여 2단계 인증을 활성화합니다.

2단계 인증을 활성화했다면, 보안에서 앱 비밀번호를 클릭합니다. 만약 보이지 않는다면 상단 검색 창에서 앱 비밀번호를 검색해서 이동합니다.

 

App name에 앱 별명이나 이름을 넣고 만들기를 클릭해줍니다. app name은 spring과 연동하거나 서비스를 이용할 때 꼭 필요한 정보가 아니니 적당히 지으시면 됩니다.

그럼 이렇게 앱 비밀번호가 나타나는데, 꼭 복사해서 메모장에 따로 적어 두세요!!!! 해당 앱 비밀번호는 한 번 확인 버튼을 누르면 다시는 볼 수 없습니다. 물론 다시 발급 받으면 되긴 하지만, 귀찮으니 따로 적어둡시다. 

그러나 메모장에 적어 두는 것과는 별개로 절대로 외부에 노출되선 안됩니다!!!! 사용방법에서도 명시하고 있듯이 앱 비밀번호는 Google 계정에 대한 완전한 액세스 권한을 가집니다. 즉, 이 앱 비밀번호가 있으면 해당 계정에 접속이 가능하다는 뜻입니다.

따라서 git과 같이 외부에 노출되는 경우, 따로 설정파일에 올린 후 해당 파일을 gitignore에 등록해 git을 통해 외부에 노출되는 걸 방지하는 게 좋습니다. (아래 springbootd 연결 단계에서 따로 분리하는 방법을 올려 두었습니다)

 

 

 

 

2. build.gradle에 의존성 추가(maven은 maven 형태로 추가해주세요)

// javamail
    implementation 'org.springframework.boot:spring-boot-starter-mail'
// thymeleaf
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

 

javamail은 메일을 보내기 위한 의존성이고, thymeleaf는 html 템플릿에 자바 변수 값을 넣기 위해서 사용했습니다.

 

 

 

 

 

3. 설정파일(properties 혹은 yml 등) 설정해주기

application-apikey.properties

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=자신의 이메일(ex : ~@gmail.com)
spring.mail.password=앱 비밀번호
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.auth=true

위 파일은 gitignore에 등록해 git을 통해 외부에 노출되지 않도록 설정해줍니다.

참고 글 : 

2023.09.10 - [Git] - [Git] git에 올렸던 파일 cache, history 삭제하기 (feat. properties 파일에서 민감 정보 분리하기)

 

[Git] git에 올렸던 파일 cache, history 삭제하기 (feat. properties 파일에서 민감 정보 분리하기)

최근 외부 api(구글 메일, 네이버 SMS API 등)를 사용하다 보니 properties 파일에 해당 api에 필요한 민감정보가 들어가 있어서 민감정보를 gitignore에 추가해야겠다고 생각은 하고 있었는데, 이번에 GCS

easyoungcode.tistory.com

만약 git action을 돌려야 하는 상황이라면?

2023.09.11 - [Git] - [Git] git secret 폴더에 파일 추가하기

 

[Git] git secret 폴더에 파일 추가하기

민감정보가 든 파일을 .gitignore에 등록해 두면 git엔 올라가지 않는다. 그럼 git action에서 빌드될 때 오류가 뜬다. 이유는 해당 파일이 깃에 없기 때문! 그렇다고 .gitignore에 등록해 둔 파일을 해제

easyoungcode.tistory.com

 

 

application.properties

...
# 위에서 작성했던 apikey.properties를 include
spring.profiles.include=apikey

spring.mail.host=smtp.gmail.com

# 타임리프 설정
spring.thymeleaf.enabled=true
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
...

 

spring.thymeleaf.prefix=classpath:/templates/     -> html template이 있는 위치입니다. 즉, 타임리프를 사용할 위치입니다.

spring.thymeleaf.suffix=.html     -> html 형식에 사용하겠다는 뜻입니다,

 

 

 

 

4. DTO 구성 (아래는 예시코드입니다, 개인의 프로젝트 로직에 따라 변경 가능합니다)

EmailRequestDto.java

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class EmailRequestDto {
    private String email;
}

 

 

EmailResponseDto.java

@NoArgsConstructor
@Getter
public class EmailResponseDto {
    private String email;
    private int statusCode;
    private String statusName;
}

 

 

EmailAuthRequestDto.java

@AllArgsConstructor
@Getter
@Builder
public class EmailAuthRequestDto {
    private String email;
    private String authToken;
}

 

 

EmailAuthResponseDto.java

@NoArgsConstructor
@Getter
public class EmailAuthResponseDto {

    private String emailAuth;
}

 

 

 

 

 

5. resources 폴더 내에 templates 폴더 생성 - 이메일에서 사용할 html 템플릿 넣어주기

6. resources 폴더 내에 static 폴더 생성 - html 템플릿에 넣어줄 이미지 삽입

 

 

 

 

 

 

6. Service 구성 (아래는 예시코드 입니다)

EmailAuthService.java

아래는 이메일을 보내는 코드만 작성해두었습니다. 이메일 중복확인하는 if문은 삭제해주시면 됩니다.

@PropertySource("classpath:application-apikey.properties")  // smtp 사용하기 위해 불러오기
@Slf4j
@Service
public class EmailAuthService {

    private final UserRepository userRepository;
    private final EmailAuthRepository emailAuthRepository;
    private final JavaMailSender mailSender;
    private final SpringTemplateEngine templateEngine;

    public EmailAuthService(EmailAuthRepository emailAuthRepository, JavaMailSender mailSender, UserRepository userRepository, SpringTemplateEngine templateEngine) {
        this.emailAuthRepository = emailAuthRepository;
        this.mailSender = mailSender;
        this.userRepository = userRepository;
        this.templateEngine = templateEngine;
    }

    // authToken 만료 시간 (10분)
    private static final long EXPIRATION_TIME = 10 * 60 * 1000;

    // 이메일 전송 전 중복 확인, 이메일 전송 메서드 호출
    @Transactional
    public EmailResponseDto sendEmail(EmailRequestDto emailRequestDto) throws MessagingException {
        // 인증 코드 만료 시간이 지난 데이터 삭제
        deleteExpiredStatusIfExpired();

        EmailResponseDto emailResponseDto;

        // 이메일 중복 확인 로직 추가
        if (validateDuplicated(emailRequestDto.getEmail())) {

            emailResponseDto = EmailResponseDto.builder()
                    .statusCode(HttpStatus.SC_OK) // 오류코드 대신 200 부탁함
                    .statusName("duplicated")
                    .email(emailRequestDto.getEmail())
                    .build();
        } else {
        
            sendEmailVerification(emailRequestDto); // 이메일 인증 링크 발송
            
            emailResponseDto = EmailResponseDto.builder()
                    .statusCode(HttpStatus.SC_ACCEPTED)
                    .email(emailRequestDto.getEmail())
                    .statusName("success")
                    .build();
        }
        return emailResponseDto;
    }

    // authToken 발급 및 이메일 양식 설정, 전송
    @Async // 이메일 전송을 비동기로 처리하기 위해 사용되는 어노테이션
    public void sendEmailVerification(EmailRequestDto emailRequestDto) throws MessagingException {
        MimeMessage message = mailSender.createMimeMessage();
        // Multipart Message가 필요하므로 true 설정
        MimeMessageHelper helper = new MimeMessageHelper(message, true);

		// 이메일 인증토큰인 authToken을 UUID로 생성 후, String 형태로 반환
        String authToken = UUID.randomUUID().toString();
        // 인증 만료 시간 설정 (현재 시간+만료기간 10분 설정)
        LocalDateTime expireDate = LocalDateTime.now().plusSeconds(EXPIRATION_TIME / 1000);
        // 이메일 인증 테이블(EmailAuth에 저장)
        EmailAuth emailAuth = EmailAuth.builder()
                .email(emailRequestDto.getEmail())
                .authToken(authToken)
                .expireDate(expireDate)
                .build();
        emailAuthRepository.save(emailAuth);

        String subject = "우연(WOOYEON) 이메일 인증 링크입니다.";
        message.setSubject(subject); // 이메일 제목 설정
        helper.setTo(emailRequestDto.getEmail()); // 이메일 받는 사람 설정
        helper.setFrom("우연(WOOWYEON)"); // 보내는 사람 설정
        // addInline 보다 먼저 실행 되어야 함
        helper.setText(setContext(emailRequestDto.getEmail(), authToken), true);
        // addInline을 통해 local에 있는 이미지 삽입 해주기 & html에서 img src='cid:{contentId}'로 설정 해주기
        helper.addInline("wooyeonLogoImage", new ClassPathResource("static/logo_wooyeon_email.png"));

        mailSender.send(message);
    }

	// 타임리프 설정하는 코드, html 템플릿에서 자바 값 사용하기 위해 설정
    public String setContext(String email, String authToken) { 
        Context context = new Context();
        String link = "http://localhost:9005/auth/email/verify?email=" + email + "&authToken=" + authToken;
        context.setVariable("link", link); // Template에 전달할 데이터 설정
        return templateEngine.process("email_authentication", context); // email_authentication.html
    }

}

 

 

 

이메일 인증을 진행하는 코드는 다음과 같습니다(위 EmailAuthService에 추가해주시면 됩니다)

// 이메일 인증 처리
    @Transactional
    public EmailAuthResponseDto verifyEmail(EmailAuthRequestDto emailAuthRequestDto) {
        EmailAuth emailAuth = emailAuthRepository.findEmailAuthByEmailAndAuthToken(emailAuthRequestDto.getEmail(), emailAuthRequestDto.getAuthToken());
        EmailAuthResponseDto emailAuthResponseDto;

        if (emailAuth != null) {
            emailAuth.emailVerifiedSuccess();

            User user = User.builder()
                    .email(emailAuthRequestDto.getEmail())
                    .userCode(UUID.randomUUID())
                    .build();
            userRepository.save(user);

            emailAuthResponseDto = EmailAuthResponseDto.builder()
                    .emailAuth("success")
                    .userCode(user.getUserCode())
                    .build();

        } else {
            emailAuthResponseDto = EmailAuthResponseDto.builder()
                    .emailAuth("fail")
                    .userCode(null)
                    .build();
        }
//        emailAuthRepository.deleteByEmail(requestDto.getEmail());
        return emailAuthResponseDto;
    }

    // 이메일 중복 확인 로직 구현
    public boolean validateDuplicated(String email) {
        // 중복된 이메일이 이미 회원 테이블에 존재한다면 예외 처리
        if (emailAuthRepository.existsByEmail(email)) {
            return true;
        }
        return false;
    }

    // EmailAuth 있는 expiredDate가 지난 데이터 삭제
    @Transactional
    public void deleteExpiredStatusIfExpired() {
        LocalDateTime currentDateTime = LocalDateTime.now();
        emailAuthRepository.deleteExpiredRecords(currentDateTime);
    }

    public String findAuthTokenByEmail(String email) {
        EmailAuth emailAuth = emailAuthRepository.findEmailAuthByEmail(email);
        String authToken = emailAuth.getAuthToken();

        return authToken;
    }

 

 

 

 

 

7. html 템플릿 파일에 타임리프 설정해주기

/resources/templates/ 에 있는 이메일에서 사용할 html 템플릿 파일로 이동합니다.

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

 

 

 

 

8. 타임리프(Thymleaf)를 통해 HTML 템플릿(TEMPLATE)에 이미지 삽입하기

해당 html 파일에서 이미지를 삽입할 부분에 아래와 같이

cid:{service에서 helper.addInline 을 통해 지정해 준 이름}

으로 넣어줍니다.

<img
  src='cid:wooyeonLogoImage'
  alt="우연"
/>

 

 

 

 

 

9. 타임리프(Thymleaf)를 통해 HTML 템플릿(TEMPLATE)에 자바 값 삽입하기

다시 해당 html 파일에서 자바 변수를 사용할 부분에 아래와 같이

th:href="@{${service에서 context.setVariable로 지정해 준 이름}}"

으로 넣어줍니다.

<a th:href="@{${link}}"
  class="button">
인증하기</a>

 

 

 

 

 

10. RestfulAPI를 통해 소통하기 (Controller 구성)

UserController.java

// 사용자가 입력한 이메일로 인증 메일 전송
    @PostMapping(value = "/auth/email", produces = "application/json;charset=UTF-8")
    public ResponseEntity<EmailResponseDto> sendEmailVerify(@RequestBody EmailRequestDto emailRequestDto) throws MessagingException {
        EmailResponseDto responseDto = emailAuthService.sendEmail(emailRequestDto);
        return ResponseEntity.ok(responseDto);
    }

 

 

아래는 이메일 인증을 위한 API입니다 (이메일 인증을 하지 않는다면 생략 가능합니다)

    // 사용자의 이메일 인증을 위한 uri
    @PostMapping(value = "/auth/email/verify", produces = "application/json;charset=UTF-8")
    public ResponseEntity<EmailAuthResponseDto> verifyEmail(@RequestBody EmailAuthRequestDto requestDto) {
        EmailAuthResponseDto emailAuthResponseDto = emailAuthService.verifyEmail(requestDto);
        return ResponseEntity.ok(emailAuthResponseDto);
    }

 

 

 

 


이상입니다. 아래는 참고했던 글입니다.

https://born2bedeveloper.tistory.com/70

 

[Spring Boot] 이메일 보내기 (3) - html 템플릿 적용 (feat. Thymeleaf)

++) 해당 포스팅은 이전 작성한 포스팅들과 연계되어 있으므로 먼저 보고 오는 것을 추천합니다! https://born2bedeveloper.tistory.com/68?category=1038709 [Spring Boot] 이메일 보내기 (2) - 참조(cc), 첨부 파일 꽤

born2bedeveloper.tistory.com