공부메모 & 오류해결/Spring Boot

[Spring Boot + SMTP] 이메일 인증 구현

남건욱 2024. 4. 1. 10:47

목차

    반응형

    1. 개발환경

    Java 17

    Spring Boot 3.1.8

    Gradle 8.5

     

     

     

     

     

    2. 의존성 추가

        // SMTP
        implementation 'org.springframework.boot:spring-boot-starter-mail'
    
        // Redis
        implementation 'org.springframework.boot:spring-boot-starter-data-redis'

    SMTP와 Redis의 의존성을 build.gradle에 추가해 줍니다.

     

     

    3. Gmail SMTP 설정

     

    3-1

    구글로그인 후 지메일로 들어갑니다.

     

     

    3-2

    오른쪽 상단의 톱니바퀴 버튼을 누른 뒤 <모든 설정 보기> 버튼을 클릭합니다.

     

     

    3-3

     

    전달 및 POP/IMAP를 눌러주신 후 사진과 같이 체크한 뒤 <변경사항 저장> 버튼을 눌러 저장해 줍니다.

     

     

    3-4

    이제 우측 상단의 프로필버튼을 눌러서 <Google 계정 관리>를 클릭해 줍니다.

     

     

     

    3-5

    이 화면이 뜰 텐데 여기서 보안을 클릭해 줍니다

     

     

     

    3-6

    스크롤을 내린 뒤 Google에 로그인하는 방법 탭에서 <2단계 인증>을 클릭해 줍니다.

     

     

     

    3-7

    스크롤을 제일 밑으로 내리면 앱 비밀번호 탭이 있습니다. 클릭해 줍니다.

     

     

     

    3-8

    사용할 이름을 적고 만들기를 눌러줍니다

     

     

     

    3-9

    다음과 같이 비밀번호가 뜰 텐데 추후 application.properties에 작성해야 하니 따로 저장해 둡니다.

     

     

     

     

    4. Redis 설치

    https://ngwdeveloper.tistory.com/94

     

    [Redis] Redis 설치 방법

    Redis NoSQL DB의 한 종류이며 우리가 흔히 사용하는 MYSQL, Orcal DB, PostgreSQL 등 RDBMS와 다르게 NoSQL DB이다. 그렇다면 무슨 차이이고 어느 상황에 사용해야 할까? RDBMS와 NoSQL의 차이 RDBMS (관계형 DB) - 데

    ngwdeveloper.tistory.com

    설치가 안되어있으신 분들은 위 링크에 들어가서 따라 해 주시면 됩니다. 만약 설치가 되어있다면 넘어가셔도 됩니다.

     

     

     

     

     

    5. application.properties 작성

    # Email Properties
    spring.mail.host=smtp.gmail.com
    spring.mail.port=587
    spring.mail.username=지메일주소@gmail.com
    spring.mail.password=2차비밀번호
    spring.mail.properties.mail.smtp.auth=true
    spring.mail.properties.mail.smtp.starttls.enable=true

    SMTP의 properties 작성법입니다. 아까 발급받은 본인의 지메일 주소와 저장해 둔 비밀번호를 입력해 주시면 됩니다.

     

    #Redis
    spring.data.redis.host = localhost
    spring.data.redis.port = 6379

    만약 Redis설치를 방금 하시고 설정이 되지 않으신 상태라면 위 코드를 추가로 properties에 넣어줍니다.

     

     

     

     

     

    6. RedisConfig 작성

    @Getter
    @Configuration
    @RequiredArgsConstructor
    @EnableRedisRepositories
    public class RedisConfig {
        @Value("${spring.data.redis.host}")
        private String host;
    
        @Value("${spring.data.redis.port}")
        private int port;
    
        // 내장 / 외부 Redis 연결
        @Bean
        public RedisConnectionFactory redisConnectionFactory(){
            return new LettuceConnectionFactory(host, port);
        }
    
        /*
         * RedisConnection 에서 넘겨준 byte 값을 직렬화 RedisTemplate 은
         * Redis 데이터를 저장하고 조회하는 기능을 하는 클래스 REdis cli 를 사용해 Redis 데이터를 직접 조회할때,
         * Redis 데이터를 문자열로 반환하기 위한 설정
         */
        @Bean
        public RedisTemplate<String, String> redisTemplate(){
            RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory());
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new StringRedisSerializer());
            return redisTemplate;
        }
    }

     

     

     

     

     

    7. RedisUtil 작성

    @Service
    @RequiredArgsConstructor
    public class RedisUtil {
    
        private final StringRedisTemplate redisTemplate;
    
        public String getData(String key) {
            ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
            return valueOperations.get(key);
        }
    
        public boolean existData(String key) {
            return Boolean.TRUE.equals(redisTemplate.hasKey(key));
        }
    
        public void setDataExpire(String key, String value, long duration) {
            ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
            Duration expireDuration = Duration.ofSeconds(duration);
            valueOperations.set(key, value, expireDuration);
        }
    
        public void deleteData(String key) {
            redisTemplate.delete(key);
        }
    
    }

     

     

     

     

    8. EmailAuthResponseDto 작성

    @Getter
    public class EmailAuthResponseDto {
        private boolean success;
        private String responseMessage;
    
        public EmailAuthResponseDto(boolean success, String responseMessage){
            this.success = success;
            this.responseMessage = responseMessage;
        }
    }

    응답결과를 담을 DTO입니다. 성공, 실패 여부와 메시지를 담아서 반환합니다.

     

     

     

     

     

    9. EmailController 작성

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/api/email")
    public class EmailController {
    
        private final EmailService emailService;
    
        // 인증번호 전송
        @GetMapping("/auth")
        public EmailAuthResponseDto sendAuthCode(@RequestParam String address) {
            return emailService.sendEmail(address);
        }
    
        // 인증번호 검증
        @PostMapping("/auth")
        public EmailAuthResponseDto checkAuthCode(@RequestParam String address, @RequestParam String authCode) {
            return emailService.validateAuthCode(address, authCode);
        }
    }

    사용할 컨트롤러입니다. 인증번호를 전송하고 검증하는 두 개의 컨트롤러만 사용합니다. 필요한 게 있다면 추가로 작성하셔서 사용하시면 됩니다.

     

     

     

     

     

    10. EmailService 작성

    @Service
    @RequiredArgsConstructor
    public class EmailService {
    
        @Value("${spring.mail.username}")
        private String senderEmail;
    
        private final JavaMailSender mailSender;
        private final RedisUtil redisUtil;
    
        public EmailAuthResponseDto sendEmail(String toEmail) {
            if (redisUtil.existData(toEmail)) {
                redisUtil.deleteData(toEmail);
            }
    
            try {
                MimeMessage emailForm = createEmailForm(toEmail);
                mailSender.send(emailForm);
                return new EmailAuthResponseDto(true, "인증번호가 메일로 전송되었습니다.");
            } catch (MessagingException | MailSendException e) {
                return new EmailAuthResponseDto(false, "메일 전송 중 오류가 발생하였습니다. 다시 시도해주세요.");
            }
        }
    
        private MimeMessage createEmailForm(String email) throws MessagingException {
    
            String authCode = String.valueOf(ThreadLocalRandom.current().nextInt(100000, 1000000));
    
            MimeMessage message = mailSender.createMimeMessage();
            message.setFrom(senderEmail);
            message.setRecipients(MimeMessage.RecipientType.TO, email);
            message.setSubject("인증코드입니다.");
            message.setText(setContext(authCode), "utf-8", "html");
    
            redisUtil.setDataExpire(email, authCode, 10 * 60L); // 10분
    
            return message;
        }
    
        private String setContext(String authCode) {
            String body = "";
            body += "<h4>" + "인증 코드를 입력하세요." + "</h4>";
            body += "<h2>" + "[" + authCode + "]" + "</h2>";
            return body;
        }
    
        public EmailAuthResponseDto validateAuthCode(String email, String authCode) {
            String findAuthCode = redisUtil.getData(email);
            if (findAuthCode == null) {
                return new EmailAuthResponseDto(false, "인증번호가 만료되었습니다. 다시 시도해주세요.");
            }
    
            if (findAuthCode.equals(authCode)) {
                return new EmailAuthResponseDto(true, "인증 성공에 성공했습니다.");
    
            } else {
                return new EmailAuthResponseDto(false, "인증번호가 일치하지 않습니다.");
            }
        }
    }

    서비스 코드입니다. message에 담을 내용을 set해준 뒤 반환하는 메서드, 인증코드를 입력하도록 바디에 넣어서 반환하는 메서드, 인증번호를 검증하는 메서드로 이루어져 있습니다.

     

     

     

     

     

    11. 테스트

    인증번호를 수신할 이메일을 address 값으로 지정해 준 뒤 GET 해주면 정상적으로 보내졌다는 true와 메시지, 200 코드가 반환됩니다.

     

     

    메시지함에 들어가도 정상적으로 수신된 걸 확인할 수 있습니다.

     

     

    RequestParam방식으로 address에는 인증할 이메일, authCode에 인증번호를 넣고 POST 해본 결과 정상적으로 인증에 성공된 것을 확인할 수 있습니다.

    반응형
    프로필사진

    남건욱's 공부기록