[Spring] 이메일 인증(Google)

Kyungmin·2024년 3월 17일
0

Spring

목록 보기
7/39

이번 포스팅은 배달 서비스 프로젝트를 진행하면서 구현하게된 이메일 인증에 대해 알아보겠다.

  • 블로그를 찾아보면서 다른 분들이 구글을 많이 이용하셨길래 나도 구글 이메일인증을 사용해보기로 하였다.

구글로 이메일 인증하기

  • 먼저 구글에 로그인을 진행한다.
    1 ) 구글 홈페이지 오른쪽 상단에 본인 프로필 클릭
    2 ) Google 계정관리 클릭
    3 ) 보안 - 검색창에 앱 비밀번호 검색

  • 보안 - 검색창에 앱 비밀번호 검색까지 잘 진행하였다면
    1 ) 본인은 이미 생성을 하여 다음과 같이 이미 존재한다.
    2 ) 만약 없다면 앱 이름을 치고 만들기를 클릭한다.
    3 ) 생성되는 16자리 비밀번호를 기억한다.(복사하기)

Build.gradle 설정

// email
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'

이건 블로그마다 조금씩 다른거 같다. 잘 돌아간다면 패스

application.yml

spring:
	mail:
    host: smtp.gmail.com
    port: 587
    username: 본인 구글 이메일@gmail.com
    password: 위에서 받은 16자리 번호 붙여서 넣기
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
          connectiontimeout: 5000
          timeout: 5000
          writetimeout: 5000
    auth-code-expiration-millis: 1800000  

이제 구글 이메일 인증을 위한 세팅은 끝났다. 본격적으로 코드로 구현해보자

EmailProperties

JavaMailSender 인터페이스를 구현하는 클래스. JavaMailSender 인터페이스는 JavaMail API를 사용하여 이메일을 전송하는 데 사용된다.

  • application.yml에 설정한 환경 변수들을 사용하여 JavaMailSenderImpl 객체를 생성한다. 이 객체를 사용해서 이메일을 보낼 수 있다.
@Configuration
public class EmailProperties {

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

    @Value("${spring.mail.port}")
    private int port;

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

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

    @Value("${spring.mail.properties.mail.smtp.auth}")
    private boolean auth;

    @Value("${spring.mail.properties.mail.smtp.starttls.enable}")
    private boolean starttlsEnable;

    @Value("${spring.mail.properties.mail.smtp.starttls.required}")
    private boolean starttlsRequired;

    @Value("${spring.mail.properties.mail.smtp.connectiontimeout}")
    private int connectionTimeout;

    @Value("${spring.mail.properties.mail.smtp.timeout}")
    private int timeout;

    @Value("${spring.mail.properties.mail.smtp.writetimeout}")
    private int writeTimeout;

    @Bean
    public JavaMailSender javaMailSender() {
        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        mailSender.setHost(host);
        mailSender.setPort(port);
        mailSender.setUsername(username);
        mailSender.setPassword(password);
        mailSender.setDefaultEncoding("UTF-8");
        mailSender.setJavaMailProperties(getMailProperties());

        return mailSender;
    }

    private Properties getMailProperties() {
        Properties properties = new Properties();
        properties.put("mail.smtp.auth", auth);
        properties.put("mail.smtp.starttls.enable", starttlsEnable);
        properties.put("mail.smtp.starttls.required", starttlsRequired);
        properties.put("mail.smtp.connectiontimeout", connectionTimeout);
        properties.put("mail.smtp.timeout", timeout);
        properties.put("mail.smtp.writetimeout", writeTimeout);

        return properties;
    }
}

EmailTool

메서드에서 확인가능하듯이 메일발송을 담당한다. 이메일 발송 성공 여부를 log로 확인하도록 하였다.

  • createEmailForm(): 발송할 이메일 데이터를 설정하는 메서드이다. 수신자 이메일 주소, 이메일 제목, 이메일 내용을 입력 받아 SimpleMailMessage 객체를 생성하여 반환한다.
  • sendEmail( ): 이메일을 발송하는 메서드 파라미터로 이메일 주소, 이메일 제목, 이메일 내용을 입력 받아 이메일 creataEmailForm() 메서드로 넘겨준다. createForm() 메서드가 SimpleMailMessage 객체를 생성하여 반환하면 주입 받은 emailSender.send() 메서드에 담아 메일을 발송한다.
@Slf4j
@Component
@Transactional
@RequiredArgsConstructor
public class EmailTool {

    private final JavaMailSender javaMailSender;


    public void sendEmail(String email, String title, String text) {
        SimpleMailMessage emailForm = createEmailForm(email, title, text);
        try {
            javaMailSender.send(emailForm);
            log.info("이메일 발송 성공");
        } catch (Exception e) {
            log.error("이메일 발송 오류");
        }
    }


    // 발신할 이메일 데이터 세팅
    private SimpleMailMessage createEmailForm(String toEmail, String title, String text) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(toEmail);
        message.setSubject(title);
        message.setText(text);

        return message;
    }
}

EmailVerificationController

이메일 인증 번호 요청과 인증 번호 검증을 요청 API를 생성하는 컨트롤러

  • sendMessage(): 이메일 전송 API. 이메일을 파라미터로 받아 해당 MemberService.sendCodeToEmail() 메서드로 넘겨준다.
  • verificationEmail(): 이메일 인증을 진행하는 API. 이메일과 사용자가 작성한 인증 코드를 파라미터로 받아 MemberService.verifiedCode() 메서드로 넘긴다. 인증에 성공하면 ture를 실패하면 false를 반환한다.
@Slf4j
@RestController
@RequestMapping("/email-verification")
@RequiredArgsConstructor
public class EmailVerificationController {

    private final EmailVerificationService emailVerificationService;


    @PostMapping
    public ResponseEntity<?> sendCode(
            @Schema(description = "인증번호를 보낼 email 주소", example = "example@example.com")
            @RequestParam("email")
            String email) {
        emailVerificationService.sendCodeToEmail(email);
        return ResponseEntity.ok().body("발송완료");
    }


    @GetMapping
    public ResponseEntity<?> verifyCode(
            @Schema(description = "인증번호를 보낸 email 주소", example = "example@example.com")
            @RequestParam("email")
            String email,
            @Schema(description = "받은 인증번호", example = "123456")
            @RequestParam("code")
            String code) {
        return ResponseEntity.ok().body(emailVerificationService.verifyCode(email, code));
    }
}

EmailVerificationService

이메일 인증 번호 생성 및 검증을 담당하는 서비스 로직

  • creatCode(): 6자리의 랜덤한 인증 코드를 생성하여 반환하는 메서드
  • checkDuplicatedEmail(): 회원가입하려는 이메일로 이미 가입한 회원이 있는지 확인하는 메서드. 만약 해당 이메일을 가진 회원이 존재하면 예외를 발생한다.
  • sendCodeToEmail(): 인증 코드를 생성 후 수신자 이메일로 발송하는 메서드. 이후 인증 코드를 검증하기 위해 생성한 인증 코드를 Redis에 저장한다.
  • verifiedCode(): 인증 코드를 검증하는 메서드. 파라미터로 전달받은 이메일을 통해 Redis에 저장되어 있는 인증 코드를 조회한 후 파라미터로 전달 받은 인증 코드와 비교한다. 만약 두 코드가 동일하다면 true를, Redis에서 Code가 없거나 일치하지 않는다면 false를 반화한다. 반환된 boolean 값은 DTO로 변환하여 반환한다.
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class EmailVerificationService {

    private final UserRepository userRepository;

    private final RedisTool redisTool;

    private final EmailTool emailTool;
    private static final String AUTH_CODE_PREFIX = "AuthCode ";

    @Value("${spring.mail.auth-code-expiration-millis}")
    private long authCodeExpirationMillis;

    public void sendCodeToEmail(String toEmail) {
        //이메일 중복 검사
        if(userRepository.existsByEmail(toEmail)) throw new CustomException(ErrorCode.DUPLICATE_EMAIL);

        //인증코드 생성, 저장 및 이메일 전송
        String title = "유저 이메일 인증 번호";
        String authCode = this.createCode();
        // 이메일 인증 요청 시 인증 번호 Redis에 저장
        redisTool.setValues(AUTH_CODE_PREFIX + toEmail,
                authCode, Duration.ofMillis(authCodeExpirationMillis));
        emailTool.sendEmail(toEmail, title, authCode);
    }


    public boolean verifyCode(String email, String authCode) {
        String redisAuthCode = redisTool.getValues(AUTH_CODE_PREFIX + email);

        return redisTool.checkExistsValue(redisAuthCode) && redisAuthCode.equals(authCode);
    }

    private String createCode() {
        try {
            Random random = SecureRandom.getInstanceStrong();
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < 6; i++) {
                builder.append(random.nextInt(10));
            }
            return builder.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new CustomException(ErrorCode.SERVER_ERROR);
        }
    }
}

Redis

  • 위 서비스 코드에서 보면 redis 관련 코드가 사용된 것을 알 수 있다.

Redis 가 뭔데?

  • Key, Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터 베이스 관리 시스템 (DBMS)
  • 데이터베이스, 캐시, 메세지 브로커로 사용되며 인메모리 데이터 구조를 가진 저장소

DB 가 있는데 왜 Redis 를 써?

데이터 베이스는 데이터를 물리 디스크에 직접 쓰기 때문에 서버에 문제가 발생하여 다운되더라도 데이터가 손실되지 않는다. 하지만 매번 디스크에 접근해야 하기 때문에 사용자가 많아질수록 부하가 많아져서 느려질 수 있다.

일반적으로 서비스 운영 초반이거나 규모가 작은, 사용자가 많지 않은 서비스의 경우에는 WEB - WAS - DB 의 구조로도 데이터 베이스에 무리가 가지 않는다.
하지만 사용자가 늘어난다면 데이터 베이스가 과부하 될 수 있기 때문에 이때 캐시 서버를 도입하여 사용한다.
그리고 이 캐시 서버로 이용할 수 있는 것이 바로 Redis 이다.

Redis 에 들어가는 데이터는?

KEY : 인증코드
VALUE : 이메일 난수

프로젝트에 Redis 의존성 추가

//REDIS
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.2.0'

application.yml (Redis)

data:
  redis:
    port: 6379
    host: localhost

RedisTool

@Slf4j
@Component
@Transactional
@RequiredArgsConstructor
public class RedisTool {
   private final RedisTemplate<String, Object> redisTemplate;

   public void setValues(String key, String data) {
       ValueOperations<String, Object> values = redisTemplate.opsForValue();
       values.set(key, data);
   }

   public void setValues(String key, String data, Duration duration) {
       ValueOperations<String, Object> values = redisTemplate.opsForValue();
       values.set(key, data, duration);
   }

   @Transactional(readOnly = true)
   public String getValues(String key) {
       ValueOperations<String, Object> values = redisTemplate.opsForValue();
       if (values.get(key) == null) {
           return "false";
       }
       return (String) values.get(key);
   }

   public boolean checkExistsValue(String value) {
       return !value.equals("false");
   }
}

RedisConfig


@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisConfig {
    private final RedisProperties redisProperties;

    // RedisProperties로 yaml에 저장한 host, post를 연결
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }

    // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        return redisTemplate;
    }
}

.
.

참고 자료
📝 https://green-bin.tistory.com/83
.
.

이후 과정은 컨트롤러의 API를 통하여 이메일을 인증하고 검증할 수 있다.

profile
Backend Developer

0개의 댓글

관련 채용 정보