[Lettrip] Redis를 이용한 이메일 인증 기능 구현

subbni·2023년 4월 10일

Lettrip

목록 보기
5/7

기본 로직

  1. 클라이언트가 이메일 인증 코드를 요청한다.
  2. 서버는 이메일 중복 여부를 확인한 뒤, 이메일 인증코드를 보내준다.
  3. 클라이언트는 해당 이메일로 수신된 이메일 인증코드로 검증을 요청한다.
  4. 서버는 해당 인증코드를 검증하여 결과를 전송한다.

결국 서버에서는 이메일 인증코드를 생성하여 클라이언트에게 보내준 뒤,
그 이메일 인증코드를 보관하고 있다가, 클라이언트가 인증코드 확인을 요청하면 그 코드가 올바른 코드인지를 확인할 수 있어야 한다.

여기서는 그 이메일 인증코드를 인메모리 데이터 저장소인 Redis에 저장한다.

Redis 관련 설정

Docker로 Redis Docker Image 생성

  • Docker Desktop 을 이용하여 redis_lettrip 이미지를 생성하였다.

    redis > run

    원하는 컨테이너 이름과 포트를 적어주고 run을 클릭하면 된다.

그럼 요로코롬 생성 완료

프로젝트에 의존 라이브러리 추가

	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
  • 이메일 인증코드를 들고 있을 데이터베이스로 Redis를 사용한다.

application.yml 파일에 사용자 정의 property 추가

spring boot 3.0이후로 spring.redis 가 spring.data.redis로 변경되었다.

# redis
spring:
  data:
    redis:
      port: 8081
      expireMinutes: 3
      host: localhost

RedisConfig

@Configuration
public class RedisConfig {
    @Value("${spring.data.redis.host}")
    private String host;

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host,port);
    }

    @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;
    }
}

Mail 관련 설정

프로젝트에 의존 라이브러리 추가

	implementation 'org.springframework.boot:spring-boot-starter-mail'
  • 사용자에게 이메일 인증코드를 보내주어야 하므로 mail 라이브러리 추가

application.yml 파일에 설정 추가

spring:
  mail:
    host: smtp.naver.com
    port: 465
    username: 네이버 아이디
    password: 패스워드
    from: 네이버 아이디@naver.com
    properties:
      mail:
        smtp:
          auth: true
          ssl:
            enable: true
            trust: smtp.naver.com
  • naver의 stmp 서비스를 이용하여 구현한다.

    📌 이 때, 만일 2단계 인증이 되어있는 네이버 아이디를 사용할 경우,
    password에 실제 네이버 비밀번호가 아닌 따로 생성되는 어플리케이션 비밀번호를 입력해주어야 한다. 위의 2단계 인증을 클릭해서 들어가면 어플리케이션 비밀번호를 생성하는 기능이 있다.

MailConfig


@Configuration
public class MailConfig {
    @Value("${spring.mail.host}")
    private String host;

    @Value("${spring.mail.from}")
    private String fromAddress;
    @Value("${spring.mail.password}")
    private String password;
    @Value("${spring.mail.port}")
    private int port;

    @Bean
    public JavaMailSender javaMailService() {
        JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();

        javaMailSender.setHost(host);
        javaMailSender.setUsername(fromAddress);
        javaMailSender.setPassword(password);

        javaMailSender.setPort(port);
        javaMailSender.setJavaMailProperties(getMailProperties());

        return javaMailSender;
    }

    private Properties getMailProperties() {
        Properties properties = new Properties();
        properties.setProperty("mail.transport.protocol", "smtp");
        properties.setProperty("mail.smtp.auth", "true");
        properties.setProperty("mail.smtp.starttls.enable", "true");
        properties.setProperty("mail.debug", "true");
        properties.setProperty("mail.smtp.ssl.trust","smtp.naver.com");
        properties.setProperty("mail.smtp.ssl.enable","true");
        return properties;
    }
}

이메일 인증 기능 구현

RedisUtil

@RequiredArgsConstructor
@Component
public class RedisUtil {
    private final StringRedisTemplate redisTemplate;

    public String getData(String key) {
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }

    public void setData(String key, String value) {
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key,value);
    }

    public void setDataExpire(String key, String value, long durationMin) {
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        Duration expireDuration = Duration.ofMinutes(durationMin);
        valueOperations.set(key,value,expireDuration);
    }

    public void deleteData(String key) {
        redisTemplate.delete(key);
    }
}

일단 기본적으로 데이터를 가져오고, 저장하고, 삭제하는 기능만 구현하였다.

MailService


@RequiredArgsConstructor
@Service
public class MailService {
    private final JavaMailSender javaMailSender;
    private final RedisUtil redisUtil;

    @Value("${spring.data.redis.expireMinutes}")
    private long expireMin;
    @Value("${spring.mail.from}")
    private String fromAddress;

    public void sendMail(String email, MimeMessage message) throws Exception{
        try{
            javaMailSender.send(message);
        } catch(MailException mailException) {
            mailException.printStackTrace();
            throw new IllegalAccessException();
        }
    }

    @Transactional
    public void sendVerificationEmail(String email) throws Exception {
        String code = createRandomCode(6);
        MimeMessage mimeMessage = createVerificationMessage(email,code);
        sendMail(email,mimeMessage);
        redisUtil.setDataExpire(code,email,expireMin);
    }

    private MimeMessage createVerificationMessage(String email, String code) throws Exception{
        MimeMessage message = javaMailSender.createMimeMessage();

        message.addRecipients(Message.RecipientType.TO,email);
        message.setSubject("Lettrip 이메일 인증 코드입니다.");
        message.setText(createVerificationEmailText(code),"utf-8","html");
        message.setFrom(new InternetAddress(fromAddress,"Lettrip"));
        return message;
    }

    private String createVerificationEmailText(String code) {
        String text = "";
        text += "아래 코드를 Lettrip 이메일 인증 코드란에 입력해주세요.";
        text += "<br>";
        text += "CODE : <strong>";
        text += code;
        text += "</strong>";

        return text;
    }

    @Transactional
    public void verifyEmailCode(String code) {
        String email = redisUtil.getData(code);
        if(email==null) {
            throw new LettripException(LettripErrorCode.EMAIL_CODE_NOT_MATCH);
        }
        redisUtil.deleteData(code);
    }

    public String createRandomCode(int length) {
        return UUID.randomUUID().toString().substring(0,length);
    }
}

실질적 이메일 전송을 맡는 클래스이다

  • 6글자의 랜덤 문자열을 만들어 이메일과 함께 redis에 저장한 뒤 사용자 이메일로 메일을 전송한다.
  • 인증코드 검증 요청이 들어오면 해당 코드가 redis에 존재하는지 확인한 뒤 존재한다면 code를 삭제한다.

AuthService

@Service
@RequiredArgsConstructor
public class AuthService {
    private final UserRepository userRepository;

    @Transactional
    public SignUpUser.Response createUser(SignUpUser.Request request) {
        checkIfDuplicatedEmail(request.getEmail());
        return SignUpUser.Response.fromEntity(createUserFromRequest(request));
    }

    private User createUserFromRequest(SignUpUser.Request request) {
        return userRepository.save(User.builder()
                .email(request.getEmail())
                .password(request.getPassword())
                .name(request.getName())
                .nickname(request.getNickname())
                .imageUrl(request.getImageUrl())
                .providerType(ProviderType.LOCAL)
                .build());
    }

    public void checkIfDuplicatedEmail(String email) {
        userRepository.findByEmail(email)
                .ifPresent(user -> {
                    throw new LettripException(LettripErrorCode.DUPLICATED_EMAIL);
                });
    }
}

이메일이 존재하는 지 확인하는 checkIfDuplicatedEmail 메서드를 아예 void로 빼서 공통적으로 사용할 수 있도록 하였다.

AuthController

@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class AuthController {
    private final AuthService authService;
    private final MailService mailService;

... 

    @GetMapping("/email-code/{email}")
    public ApiResponse sendEmailVerificationCode(@PathVariable String email) throws Exception {
        authService.checkIfDuplicatedEmail(email);
        mailService.sendVerificationEmail(email);
        return new ApiResponse(true, "이메일 인증 코드가 메일로 전송되었습니다.");
    }

    @GetMapping("/email-verify/{code}")
    public ApiResponse verifyEmailCode(@PathVariable String code) {
        mailService.verifyEmailCode(code);
        return new ApiResponse(true, "이메일 인증이 완료되었습니다.");
    }
}

실행되는 모든 메서드들의 로직에서 오류가 발생할 경우엔 오류 메세지가 응답으로 전송되고, 에러 없이 성공적으로 모든 로직이 성공된다면 ApiResponse에 확인 메세지가 담겨 전달된다.

ApiResponse.java

@Getter
@Setter
@RequiredArgsConstructor
public class ApiResponse {
    private  boolean success;
    private  String message;
    public ApiResponse(boolean success,String message) {
        this.success = success;
        this.message = message;
    }
} 

Test

이메일 인증 코드 요청

이메일 도착 확인

이메일 인증 코드 검증 요청


성공적으로 검증 되는 것을 볼 수 있다.

이메일 인증 실패 시나리오

1 . 회원가입 된 이메일로 인증 요청


test@naver.com의 이메일로 가입한 회원이 있는 상태에서 이메일 인증 요청 시

DUPLICATED_EMAIL 에러가 잘 발생하는 것을 확인!

2 . 잘못된 인증 코드 입력


EMAIL_CODE_NOT_MATCH 에러가 잘 발생하는 것을 확인 !

profile
개발콩나물

0개의 댓글