스프링 이메일 인증 구현하기

진기·2024년 3월 14일

이메일 인증을 구현하려면 이메일 서버가 필요하다. 직접 이메일 서버를 만드는 것보다 네이버나 구글의 이메일 서버를 빌리는 것이 보안적인 측면, 구현적인 측면에서 좋을 것이라 판단하였고 이 글은 구글 이메일 서버를 빌려 진행할 것이다.

구글 서버 사용하기

1) 구글 이메일에 접속한다. https://www.google.com/intl/ko/gmail/about/
2) 내 프로필을 클릭하고 Google 계정 관리에 들어간다.
3) 앱 비밀번호가 필요한데 이것을 만들려면 우리의 구글 계정에 2단계 인증이 필요하다. 보안탭에서 2단계 인증을 사용하는 로그인을 설정해준다.
4) 설정을 해준 뒤 앱 비밀번호를 생성해준다.
5) 생성 된 앱 비밀번호를 따로 저장해준다.(필수는 아님)

Build.gradle 설정하기

JavaMailApi를 직접 사용하기 위한 의존성

implementation 'javax.mail:mail:1.4.7'
	// Spring Context Support
	implementation 'org.springframework:spring-context-support:5.3.9'

스프링부트에서 제공하는 편리한 이메일 전송 기능을 위한 의존성으로, 스프링 생태계 내에서 통합되고 설정이 간단하게 처리

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '2.6.3'

build.gradle에 추가해준다.(2가지 방식 모두에서 작동되는 것을 확인 하였다.)

Configuration 만들기

EmailConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import java.util.Properties;
@Configuration
public class EmailConfig {
    @Bean
    public JavaMailSender mailSender() { //JAVA MAILSENDER 인터페이스를 구현한 객체를 빈으로 등록하기 위함.

        JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); //JavaMailSender 의 구현체를 생성하고
        mailSender.setHost("smtp.gmail.com"); // 속성을 넣음, 이메일 전송에 사용할 SMTP 서버 호스트를 설정
        mailSender.setPort(587);// 587로 포트를 지정
        mailSender.setUsername("구글@gmail.com"); //구글계정
        mailSender.setPassword("아까 저장한 앱비밀번호"); //구글 앱 비밀번호

        Properties javaMailProperties = new Properties(); //JavaMail의 속성을 설정하기 위해 Properties 객체를 생성
        javaMailProperties.put("mail.transport.protocol", "smtp"); //프로토콜로 smtp 사용
        javaMailProperties.put("mail.smtp.auth", "true"); //smtp 서버에 인증이 필요
        javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); //SSL 소켓 팩토리 클래스 사용
        javaMailProperties.put("mail.smtp.starttls.enable", "true");//STARTTLS(TLS를 시작하는 명령)를 사용하여 암호화된 통신을 활성화
        javaMailProperties.put("mail.debug", "true"); //디버깅 정보 출력
        javaMailProperties.put("mail.smtp.ssl.trust", "smtp.gmail.com"); //smtp 서버의 ssl 인증서를 신뢰
        javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2"); //사용할 ssl 프로토콜 버젼 

        mailSender.setJavaMailProperties(javaMailProperties);//mailSender에 우리가 만든 properties 넣고 

        return mailSender;//빈으로 등록
    }
}

DTO 생성

사용자의 이메일을 받아올 때 DTO로 받는다.

EmailRequestDTO

@Getter
@Setter
public class EmailRequestDTO {
    @Email
    //1)@기호를 포함해야 한다.
    //2)@기호를 기준으로 이메일 주소를 이루는 로컬 호스트와 도메인 파트가 존재해야 한다.
    //3)도메인 파트는 최소 하나의 점과 그 뒤에 최소한 2개의 알파벳을 가진다는 것을 검증
    @NotEmpty(message = "이메일을 입력해 주세요.")
    private String email;
}

Controller 생성

MailController

@RestController
@RequiredArgsConstructor
public class MailController {

    private final MailSendService mailService;

    @PostMapping ("/mailSend")
    public String mailSend(@RequestBody @Valid EmailRequestDTO emailDTO){
        System.out.println("이메일 인증 요청이 들어옴");
        System.out.println("이메일 인증 이메일 :" + emailDTO.getEmail());
        return mailService.joinEmail(emailDTO.getEmail());
    }
  • mailSend
    사용자에게 메일을 보내는 기능을 구현하는 메서드

메일 Service 생성

인증 번호를 생성하고 이메일을 보내는 서비스를 수행한다.

MailSendService

@Service
public class MailSendService {
    @Autowired
    private JavaMailSender mailSender;
    private int authNumber;

    //임의의 6자리 양수를 반환
    public void makeRandomNumber() {
        Random r = new Random();
        String randomNumber = "";
        for(int i = 0; i < 6; i++) {
            randomNumber += Integer.toString(r.nextInt(10));
        }

        authNumber = Integer.parseInt(randomNumber);
    }


    //mail을 어디서 보내는지, 어디로 보내는지 , 인증 번호를 html 형식으로 어떻게 보내는지 작성
    public String joinEmail(String email) {
        makeRandomNumber();
        String setFrom = "wlsrl515@gmail.com"; // email-config에 설정한 자신의 이메일 주소를 입력
        String toMail = email;
        String title = "회원 가입 인증 이메일 입니다."; // 이메일 제목
        String content =
                "Traveler를 방문해주셔서 감사합니다." + 	//html 형식으로 작성 !
                        "<br><br>" +
                        "인증 번호는 " + authNumber + "입니다." +
                        "<br>" +
                        "인증번호를 제대로 입력해주세요"; //이메일 내용 삽입
        mailSend(setFrom, toMail, title, content);
        return Integer.toString(authNumber);
    }

    //이메일 전송
    public void mailSend(String setFrom, String toMail, String title, String content) {
        MimeMessage message = mailSender.createMimeMessage();//JavaMailSender 객체를 사용하여 MimeMessage 객체를 생성
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message,true,"utf-8");//이메일 메시지와 관련된 설정을 수행합니다.
            // true를 전달하여 multipart 형식의 메시지를 지원하고, "utf-8"을 전달하여 문자 인코딩을 설정
            helper.setFrom(setFrom);//이메일의 발신자 주소 설정
            helper.setTo(toMail);//이메일의 수신자 주소 설정
            helper.setSubject(title);//이메일의 제목을 설정
            helper.setText(content,true);//이메일의 내용 설정 두 번째 매개 변수에 true를 설정하여 html 설정
            mailSender.send(message);
        } catch (MessagingException e) {//이메일 서버에 연결할 수 없거나, 잘못된 이메일 주소를 사용하거나, 인증 오류가 발생하는 등 오류
            // 이러한 경우 MessagingException이 발생
            e.printStackTrace();//e.printStackTrace()는 예외를 기본 오류 스트림에 출력하는 메서드
        }
        
    }

}

이로써 사용자에게 메일을 보내긴 했다. 또한 인증번호를 프론트엔드 쪽에다 JSON 형식으로 보내는 것을 완료했다. 이제 사용자가 인증번호를 입력하고 올바르게 입력 하였는지 확인하고 싶다. 그렇다면 사용자의 원래 인증번호를 DB에다가 저장해야 할까?? 단 몇 분만 지속되는 인증번호를 위해 DB에다가 저장하기엔 효율적이지 못하다고 생각한다. 그렇기 때문에 인메모리 DB인 Redis를 사용하여 사용자의 인증번호를 짧게 저장한다.

Redis 다운로드

https://github.com/microsoftarchive/redis/releases 이 곳에서 다운로드를 진행한다.

Build.gradle 설정

build.gradle 에 추가한다.

//redis 추가
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'

application.yml 설정

  # Redis configuration
  redis:
    host: localhost
    port: 6379

DTO 생성

사용자가 인증번호를 확인하고 인증번호를 입력하였을 때 받아오는 DTO 이다.

EmailCheckDTO

@Data
public class EmailCheckDTO {

    @Email
    @NotEmpty(message = "이메일을 입력해 주세요")
    private String email;

    @NotEmpty(message = "인증 번호를 입력해 주세요")
    private String authNum;
}

Controller 수정

MailController

@RestController
@RequiredArgsConstructor
public class MailController {

    private final MailSendService mailService;

    @PostMapping ("/mailSend")
    public String mailSend(@RequestBody @Valid EmailRequestDTO emailDTO){
        System.out.println("이메일 인증 요청이 들어옴");
        System.out.println("이메일 인증 이메일 :" + emailDTO.getEmail());
        return mailService.joinEmail(emailDTO.getEmail());
    }

    @PostMapping("/mailauthCheck")
    public String AuthCheck(@RequestBody @Valid EmailCheckDTO emailCheckDTO){
        Boolean Checked=mailService.CheckAuthNum(emailCheckDTO.getEmail(), emailCheckDTO.getAuthNum());
        if(Checked){
            return "ok";
        }
        else{
            throw new NullPointerException("다시 시도해 주세요!");
        }
    }

}
  • AuthCheck
    사용자가 이메일과 인증번호를 주었을 때 인증번호가 맞는지 확인한다. 인증번호가 맞다면 ok가 나올 것이고 사용자가 입력한 인증번호가 아니라면 NullPonterException 예외를 터트릴 것이다.

예외를 다루기 위한 클래스

예외가 터졌을 때 JSON으로 그 내용을 확인한다.

ErrorResult

@Data
@AllArgsConstructor
public class ErrorResult {
  private String code;
  private String message;
}

ExControllerAdvice

NullPointer 예외가 발생했을 때 이렇게 응답한다.

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

// NullPointer 예외가 발생했을 때 응답 방식
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity<ErrorResult> testing(NullPointerException e) {
        ErrorResult errorResult = new ErrorResult("EMAIL", e.getMessage());
        return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
    }
}

메일 서비스에 인증까지 추가

MailSendService

@Service
@RequiredArgsConstructor
public class MailSendService {

    private final JavaMailSender mailSender;
    private final RedisUtil redisUtil;
    private int authNumber; // 인증 번호

    //추가
    public boolean CheckAuthNum(String email, String authNum) {
        if (redisUtil.getData(authNum) == null) {
            return false;
        } else if (redisUtil.getData(authNum).equals(email)) {
            return true;
        } else {
            return false;
        }
    }

    //임의의 6자리 양수를 반환
    public void makeRandomNumber() {
        Random r = new Random();
        String randomNumber = "";
        for (int i = 0; i < 6; i++) {
            randomNumber += Integer.toString(r.nextInt(10));
        }

        authNumber = Integer.parseInt(randomNumber);
    }

    //mail을 어디서 보내는지, 어디로 보내는지 , 인증 번호를 html 형식으로 어떻게 보내는지 작성
    public String joinEmail(String email) {
        makeRandomNumber();
        String setFrom = "wlsrl515@gmail.com"; // email-config에 설정한 자신의 이메일 주소를 입력
        String toMail = email;
        String title = "회원 가입 인증 이메일 입니다."; // 이메일 제목
        String content =
                "Traveler를 방문해주셔서 감사합니다." +    //html 형식으로 작성 !
                        "<br><br>" +
                        "인증 번호는 " + authNumber + "입니다." +
                        "<br>" +
                        "인증번호를 제대로 입력해주세요"; //이메일 내용 삽입
        mailSend(setFrom, toMail, title, content);
        return Integer.toString(authNumber);
    }

    //이메일을 전송합니다.
    public void mailSend(String setFrom, String toMail, String title, String content) {
        MimeMessage message = mailSender.createMimeMessage();//JavaMailSender 객체를 사용하여 MimeMessage 객체를 생성
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "utf-8");//이메일 메시지와 관련된 설정을 수행합니다.
            // true를 전달하여 multipart 형식의 메시지를 지원하고, "utf-8"을 전달하여 문자 인코딩을 설정
            helper.setFrom(setFrom);//이메일의 발신자 주소 설정
            helper.setTo(toMail);//이메일의 수신자 주소 설정
            helper.setSubject(title);//이메일의 제목을 설정
            helper.setText(content, true);//이메일의 내용 설정 두 번째 매개 변수에 true를 설정하여 html 설정
            mailSender.send(message);
        } catch (MessagingException e) {//이메일 서버에 연결할 수 없거나, 잘못된 이메일 주소를 사용하거나, 인증 오류가 발생하는 등 오류
            // 이러한 경우 MessagingException이 발생
            e.printStackTrace();//e.printStackTrace()는 예외를 기본 오류 스트림에 출력하는 메서드
        }
        redisUtil.setDataExpire(Integer.toString(authNumber), toMail, 60 * 5L); // 5분 동안 인증번호 유효

    }

}
  • CheckAuthNum
    사용자가 입력한 인증번호와 실제 인증 번호를 비교한다.
  • redisUtil.setDataExpire(Integer.toString(authNumber), toMail, 60 * 5L);
    5분 동안 인증 번호가 유효하다.
  • @RequiredArgsConstructor를 활용해 private final JavaMailSender mailSender;
    private final RedisUtil redisUtil; 생성자를 주입 해준다.

RedisUtil 클래스 만들기

RedisUtil

@Service
@RequiredArgsConstructor
public class RedisUtil {
    private final StringRedisTemplate redisTemplate;//Redis에 접근하기 위한 Spring의 Redis 템플릿 클래스

    public String getData(String key){//지정된 키(key)에 해당하는 데이터를 Redis에서 가져오는 메서드
        ValueOperations<String,String> valueOperations=redisTemplate.opsForValue();
        return valueOperations.get(key);
    }
    public void setData(String key,String value){//지정된 키(key)에 값을 저장하는 메서드
        ValueOperations<String,String> valueOperations=redisTemplate.opsForValue();
        valueOperations.set(key,value);
    }
    public void setDataExpire(String key,String value,long duration){//지정된 키(key)에 값을 저장하고, 지정된 시간(duration) 후에 데이터가 만료되도록 설정하는 메서드
        ValueOperations<String,String> valueOperations=redisTemplate.opsForValue();
        Duration expireDuration= Duration.ofSeconds(duration);
        valueOperations.set(key,value,expireDuration);
    }
    public void deleteData(String key){//지정된 키(key)에 해당하는 데이터를 Redis에서 삭제하는 메서드
        redisTemplate.delete(key);
    }
}

postman을 통한 테스트


인증번호가 잘 나온다는 것이 확인 가능하다.


구글 이메일함에도 인증번호가 도착한 것을 확인 가능하다.

인증번호를 입력하면 인증이 성공한 것을 확인 할 수 있다.

profile
개발 성장 이야기

0개의 댓글