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

이진우·2023년 7월 5일
3

스프링 학습

목록 보기
2/35

들어가기 전에

이메일 인증을 구현하려면 어떻게 해야 할까?
이메일 인증이 필요하려면 아무튼간에 이메일 서버가 필요할 것이다. 아무래도 우리가 이메일 서버를 직접 만드는 것보다는 네이버나 구글의 이메일 서버를 빌리는 것이 보안적인 측면, 구현적인 측면에서 좋을 것이다.

외부 이메일 서버를 사용하자

아까 말했듯 우리가 직접 구현하는 것보다는 외부 서버가 나을 것이다!

구글 서버 사용할때는?

1)일단 구글을 킨다.
2)오른쪽 상단의 내 프로필을 클릭하고 Google 계정 관리를 클릭한다.


3)우리는 앱비밀번호라는게 필요한데 앱 비밀번호를 만드려면 우리의 구글 계정에 2단계 인증이 필요하다. 보안탭에서 2단계 인증을 사용하는 로그인을 설정해 주어야 한다.
4)저 위에 검색창이 앱 비밀번호라고 치고 나오는 화면을 확인한다.

5)그러면 왼쪽에 메일 오른쪽에 Windows 컴퓨터를 누른후 생성을 해주면 된다.
6)생성을 하면 아래 화면처럼 나온다.

7)windows 컴퓨터용 앱 비밀번호를 따로 저장해둔다.

네이버 서버를 사용할때는?

1)네이버 화면에 들어간다 2)네이버 화면에서 로그인을 한다 3)네이버 화면에서 로그인을 한다면 메일 탭이 보일 것이다. 메일 탭 맨 아래에 메일이라는 링크가 있을 것이다.

4)메일에 들어간다면
이런 탭을 볼 수 있다.
환경설정으로 가자!
5)환경 설정으로 가서 POP3,IMAP 설정으로 가보자
그렇다면 POP3/SMTP 사용을 사용함으로 바꾸어주고 아래 화면처럼 세팅을 해둔다.

네이버 pop3와 imap 차이점 링크

pop3/SMTP 와 IMAP/SMTP 의 차이점을 밝혀둔 네이버 창이 있다. 밑에서 그 내용을 확인할 수 있다.

https://guide.worksmobile.com/kr/mail/mail-guide/settings/pop3-imap-smtp/

Build.gradle 설정

build.gradle 에 이것을 추가하거나(JavaMailApi를 직접 사용하기 위한 의존성)
implementation 'javax.mail:mail:1.4.7'
	// Spring Context Support
	implementation 'org.springframework:spring-context-support:5.3.9'

를 추가한다. (Spring Boot에서 제공하는 편리한 이메일 전송 기능을 위한 의존성으로, 스프링 생태계 내에서 통합되고 설정이 간단하게 처리)

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

앞으로 할 코드는 2가지 방식 모두에서 작동되는 것을 확인 하였다

Configuration 만들기

구글 이용 경우

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.naver.com");//smtp 서버의 ssl 인증서를 신뢰
        javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2");//사용할 ssl 프로토콜 버젼 

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

        return mailSender;//빈으로 등록한다.
    }
}

네이버 이용 경우

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.naver.com");// 속성을 넣기 시작합니다. 이메일 전송에 사용할 SMTP 서버 호스트를 설정
        mailSender.setPort(465);// 465로 포트를 지정
        mailSender.setUsername("네이버 ID");//네이버 ID를 넣습니다.
        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.naver.com");//smtp 서버의 ssl 인증서를 신뢰
        javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2");//사용할 ssl 프로토콜 버젼 

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

        return mailSender;//빈으로 등록한다.
    }
}

DTO 생성

사용자의 이메일을 받아올때 DTO로 받아보겠습니다.

EmailRequestDto

import lombok.Getter;
import lombok.Setter;

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

Controller 생성

import com.testtt.email.Dto.EmailCheckDto;
import com.testtt.email.Dto.EmailRequestDto;
import com.testtt.email.Service.MailSendService;
import lombok.RequiredArgsConstructor;
import org.hibernate.annotations.Check;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequiredArgsConstructor
public class MailController {
    private final MailSendService mailService;
    @PostMapping ("/mailSend")
    public String mailSend(@RequestBody @Valid EmailRequestDto emailDto){
        System.out.println("이메일 인증 이메일 :"+emailDto.getEmail());
        return mailService.joinEmail(emailDto.getEmail());
    }
    
  • mailSend
  • 사용자에게 메일을 보내는 기능을 구현하는 메서드입니다.

    메일 Service 생성

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

    MailSendService

    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.mail.javamail.JavaMailSender;
    import org.springframework.mail.javamail.MimeMessageHelper;
    import org.springframework.stereotype.Service;
    
    import javax.mail.MessagingException;
    import javax.mail.internet.MimeMessage;
    import java.util.Random;
    
    @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 = "dionisos198@naver.com"; // email-config에 설정한 자신의 이메일 주소를 입력
            String toMail = email;
            String title = "회원 가입 인증 이메일 입니다."; // 이메일 제목
            String content =
                    "나의 APP을 방문해주셔서 감사합니다." + 	//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 다운로드

    Redis 다운로드는 이 링크를 참조하였다. https://oingdaddy.tistory.com/225

    Build.gradle 설정

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

    를 build.gradle 에 추가한다.

    application.properties 설정

    spring.redis.host=localhost
    spring.redis.port=6379

    를 추가한다.

    Dto 생성

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

    EmailCheckDto

    
    import lombok.Data;
    
    import javax.validation.constraints.Email;
    import javax.validation.constraints.NotEmpty;
    
    @Data
    public class EmailCheckDto {
        @Email
        @NotEmpty(message = "이메일을 입력해 주세요")
        private String email;
    
        @NotEmpty(message = "인증 번호를 입력해 주세요")
        private String authNum;
    
    }
    

    Controller 생성

    @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

    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    @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;
    
    @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);
        }
    }
    

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

    @Service
    public class MailSendService {
        @Autowired
        private JavaMailSender mailSender;
        @Autowired
        private  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 = "dionisos198@naver.com"; // email-config에 설정한 자신의 이메일 주소를 입력
            String toMail = email;
            String title = "회원 가입 인증 이메일 입니다."; // 이메일 제목
            String content =
                    "나의 APP을 방문해주셔서 감사합니다." + 	//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);
    
        }
    
    }
  • CheckAuthNum
  • 사용자가 입력한 인증번호와 실제 인증 번호를 비교한다
  • redisUtil.setDataExpire(Integer.toString(authNumber),toMail,60*5L)
  • 5분 동안 인증번호가 살게 합니다.
  • @Autowired private RedisUtil redisUtil;
  • RedisUtil도 Autowired로 추가하였다.

    RedisUtil 클래스 만들기

    import lombok.RequiredArgsConstructor;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.ValueOperations;
    import org.springframework.stereotype.Service;
    
    import java.time.Duration;
    
    @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을 통한 테스트


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


    이렇게 보낸다면 인증이 성공한 것이 되겠다.

    참고로 key값은
    redis cli 에서
    확인 할 수 있다.

    만료기한으로 설정한 5분이 지나면 만료가 되어

    아까 설정한 NullPointerException 예외가 발생해서 지정된 형식으로 JSON 형태로 데이터가 나왔다.

    profile
    기록을 통해 실력을 쌓아가자

    3개의 댓글

    comment-user-thumbnail
    2024년 4월 11일

    좋은 글 감사합니다.
    글을 읽으며 따라하다 한가지 의문이 생겼습니다!
    인증번호 메일을 보냈을 때 컨트롤러에서 프론트단으로 인증번호를 넘겨주는데 혹시 따로 넘겨주는 이유가 있는건가요?
    사용자 입장에선 이메일을 통해 인증번호를 확인할 것이고
    인증번호에 대한 검증은 백엔드단에서 이루어지는데 굳이 넘겨준 이유가 있으신건지 궁금합니다!!
    단순히 테스트용도인가요?

    1개의 답글
    comment-user-thumbnail
    2024년 5월 28일

    감사합니다! 많은 도움이 되었습니다 :)

    답글 달기