비밀번호 찾기 & 재설정 구현(Google SMTP & Redis)

2024. 6. 3. 22:44·📘 Backend/Spring

비밀번호 찾기 & 재설정 기능 구현(Google SMTP)

사내 비밀번호 찾기/초기화 기능을 위해 Email 인증을 자체 SMTP 서버를 만들어서 하려다가, 귀찮아서 Google SMTP를 이용하기로 했습니다.


우선 Google 계정 설정에 들어가서 아래 2개의 작업을 해줍니다.

  • Multi Factor Authentication(2FA) 활성화
  • App Password 생성

그리고 Gmail 탭으로 들어가 Mail 설정의 Forwarding and POP/IMAP 탭에 들어가서 아래와 같이 설정합니다.


Spring Boot Server 설정

build.gradle 파일에 Mail 추가

implementation 'org.springframework.boot:spring-boot-starter-mail'

application.yml 파일에 메일 설정

spring: 
  mail:  
    host: smtp.gmail.com  
    port: 587  
    username: dainsdevteam  
    password: abcd 
    properties:  
      mail.smtp.debug: true  
      mail.smtp.connection timeout: 1000 #1초  
      mail.starttls.enable: true  
      mail.smtp.auth: true

Mail Config 작성

@Configuration  
@RequiredArgsConstructor  
public class MailConfig {  
    private static final String MAIL_SMTP_AUTH = "mail.smtp.auth";  
    private static final String MAIL_DEBUG = "mail.stmp.debug";  
    private static final String MAIL_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";  
    private static final String MAIL_SMTP_STARTTLS_ENABLE = "mail.smtp.starttls.enable";  

    // SMTP 서버  
    @Value("${spring.mail.host}")  
    private String host;  

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

    // 비밀번호  
    @Value("${spring.mail.password}")  
    private String password;  

    // 포트번호  
    @Value("${spring.mail.port}")  
    private int port;  

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

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

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

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

    @Bean  
    public JavaMailSender mailSender() {  
        // Sender 정보 추가  
        JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();  
        javaMailSender.setHost(host);  
        javaMailSender.setUsername(username);  
        javaMailSender.setPassword(password);  
        javaMailSender.setPort(port);  

        // Mail Properties 추가  
        Properties properties = javaMailSender.getJavaMailProperties();  
        properties.put(MAIL_SMTP_AUTH, auth);  
        properties.put(MAIL_DEBUG, debug);  
        properties.put(MAIL_CONNECTION_TIMEOUT, connectionTimeout);  
        properties.put(MAIL_SMTP_STARTTLS_ENABLE, startTlsEnable);  

        javaMailSender.setJavaMailProperties(properties);  
        javaMailSender.setDefaultEncoding("UTF-8");  

        return javaMailSender;  
    }  
}

Mail Service로 테스트 이메일 보내보기

@Slf4j  
@Service  
@Scheduled  
@Transactional(readOnly = true)  
@RequiredArgsConstructor  
public class MailService {  
    private final JavaMailSender mailSender;  

    public void sendEmail(String receiver, String title, String text) {  
        SimpleMailMessage message = new SimpleMailMessage();  
        message.setTo(receiver);  
        message.setSubject(DateUtil.getDate() + " " + title);  
        message.setText(text);  

        try {  
            mailSender.send(message);  
        } catch (RuntimeException e) {  
            log.warn("Failed to Send Email - {}", receiver);  
            throw new CommonException(CommonExceptionCode.SERVER_ERROR);  
        }  
    }  

    @Scheduled(fixedRate = 5000)  
    public void emailScheduler() {  
        sendEmail("tensorflow555@gmail.com", "테스트 이메일", "ㅇㅇㅇ");  
    }  
}

랜덤 인증번호 생성 & 인증번호 검증 함수 구현

  • 인증번호를 생성할 때 Redis Hash를 생성해 code, expiration Key/Value를 넣고 Expire를 120초로 설정해줍니다.
  • 인증번호 검증 성공 시, 즉시 Redis Hash를 제거합니다.
  • Redis Hash 제거 후, 비밀번호 초기화 및 재설정 API에서 사용할 Redis Key를 1개 만들어 "ok"를 저장해줍니다.
@Slf4j  
@Service  
@Transactional(readOnly = true)  
@RequiredArgsConstructor  
public class MailService {  
    private final AdmUserRepository admUserRepository;  
    private static final String TITLE = "비밀번호 재설정 인증번호";  
    private final JavaMailSender mailSender;  
    private final JwtProvider jwtProvider;  
    private final RedisTemplate<String, Object> redisTemplate;  

    // Send Mail  
    public void sendEmail(String receiver) {  
        SimpleMailMessage message = new SimpleMailMessage();  
        String to = receiver;  
        String code = this.generateAuthCode();  

        message.setTo(to);  
        message.setSubject(TITLE);  
        message.setText(code);  

        // Redis에 인증코드 + 수신자 저장, 120초로 만료시간 설정  
        redisTemplate.opsForHash().put(receiver, "code", code);  
        redisTemplate.opsForHash().put(receiver, "expiration", DateUtil.getTime());  
        redisTemplate.expire(receiver, 120, TimeUnit.SECONDS);  

        try {  
            mailSender.send(message);  
        } catch (RuntimeException e) {  
            log.warn("Failed to Send Email - {}", receiver);  
            throw new CommonException(CommonExceptionCode.SERVER_ERROR);  
        }  
    }  

    // 4자리 인증번호 생성  
    public String generateAuthCode() {  
        try {  
            Random random = SecureRandom.getInstanceStrong();  
            StringBuilder codeBuilder = new StringBuilder();  

            for (int i = 0; i < 4; i++) {  
                codeBuilder.append(random.nextInt(10));  
            }  

            return codeBuilder.toString();  
        } catch (Exception e) {  
            log.warn("Failed to generate auth code - {}", e.getMessage());  
            throw new CommonException(CommonExceptionCode.SERVER_ERROR);  
        }  
    }  

    // 인증코드 검증  
    public boolean verifyAuthCode(String email, String authCode) {  
        try {  
            Optional<AdmUser> optUser = admUserRepository.searchUserByEmail(email);  

            AdmUser user = null;  
            if (optUser.isPresent()) user = optUser.get();  
            else throw new CommonException(CommonExceptionCode.NOT_EXIST_USER);  

            String key = user.getEmail();  
            String now = DateUtil.getTime();  
            String redisCode = (String) redisTemplate.opsForHash().get(user.getEmail(), "code");  
            String expiration = (String) redisTemplate.opsForHash().get(user.getEmail(), "expiration");  

            // 현재 시간과 만료 시간의 차이를 milli second로 반환  
            long checkExpirationTime = DateUtil.getSecondsDifference(now, expiration);  

            // 만료시간 120초로 설정  
            if (checkExpirationTime < 120 && redisCode.equals(authCode)) {  
                // 인증 성공 시, Redis Hash 삭제  
                redisTemplate.opsForHash().delete(key, "code");  
                redisTemplate.opsForHash().delete(key, "time");  

                // 비밀번호 재설정 API 에서 사용할 Redis Key 추가(OK 사인)  
                redisTemplate.opsForValue().set(user.getEmail(), "ok");  

                return true;  
            } else {  
                redisTemplate.opsForHash().delete(key, "code");  
                redisTemplate.opsForHash().delete(key, "time");  

                return false;  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  

        return false;  
    }  
}

컨트롤러 작성

@PreAuth(componentId = 4, authorization = AuthorizationType.Create)  
@PostMapping("/generate/code")  
public ResponseEntity requestAuthCode(CustomHttpServletRequest request, @RequestParam("email") String email) { 
    mailService.sendEmail(email);  
    return new ResponseEntity(ApiResponseDto.makeSuccessResponse(), HttpStatus.OK);  
}  

@PreAuth(componentId = 4, authorization = AuthorizationType.Read)  
@GetMapping("/verify/code")  
public ResponseEntity verifyAuthCode(CustomHttpServletRequest request, @RequestParam("email") String email, @RequestParam("code") String code) {  
    return new ResponseEntity(ApiResponseDto.makeResponse(mailService.verifyAuthCode(email, code)), HttpStatus.OK);  
}

인증번호 생성 & 검증하기

PostMan으로 요청을 보내 인증번호를 받습니다.


이메일 확인


Redis Hash를 보면 코드와 만료시간이 들어가 있습니다.


인증번호 검증 API를 호출해보면 120초 이내에 요청하고, 검증이 성공해 true 반환


이후 비밀번호 재설정 및 초기화 로직은 인증코드 검증 시 마지막에 추가한 Redis Key를 이용해,

비밀번호를 새로 넣어주고 사용한 Redis Key는 제거 해주었고, 다시 로그인을 하니 비밀번호가 잘 바뀐걸 확인할 수 있습니다.

@Transactional  
public void generateNewPassword(final Integer userId, final String password) {  
    AdmUser user = admUserRepositoryInf.findAdmUserByUserId(userId);  

    if (user != null) {  
        String sign = (String) redisTemplate.opsForValue().get(user.getEmail());  

        if ("ok".equals(sign)) {  
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();  
            String newPassword = encoder.encode(password);  

            user.changeAdminPassword(newPassword);  
            admUserRepositoryInf.save(user);  
            redisTemplate.delete(user.getEmail());  
        } else {  
            throw new CommonException(CommonExceptionCode.ACCESS_DENIED);  
        }  
    } else {  
        throw new CommonException(CommonExceptionCode.NOT_EXIST_USER);  
    }  
}
저작자표시 (새창열림)

'📘 Backend > Spring' 카테고리의 다른 글

Spring AOP - API Verification 공통화, 로깅  (2) 2024.07.11
DataBufferLimitException - Webflux 버퍼 크기 제한 초과  (0) 2024.06.18
Spring WebSocket (Stomp X)  (1) 2024.04.26
조회 성능 최적화 - MPTT(트리 순회 방식)  (0) 2024.04.23
Discord WebHook 연동 (Spring Boot)  (0) 2024.04.09
'📘 Backend/Spring' 카테고리의 다른 글
  • Spring AOP - API Verification 공통화, 로깅
  • DataBufferLimitException - Webflux 버퍼 크기 제한 초과
  • Spring WebSocket (Stomp X)
  • 조회 성능 최적화 - MPTT(트리 순회 방식)
신건우
신건우
조용한 개발자
  • 신건우
    우주먼지
    신건우
  • 전체
    오늘
    어제
    • 분류 전체보기 (422)
      • 📘 Frontend (71)
        • Markup (1)
        • Style Sheet (2)
        • Dart (8)
        • Javascript (12)
        • TypeScript (1)
        • Vue (36)
        • React (2)
        • Flutter (9)
      • 📘 Backend (143)
        • Java (34)
        • Concurrency (19)
        • Reflection (1)
        • Kotlin (29)
        • Python (1)
        • Spring (42)
        • Spring Cloud (5)
        • Message Broker (5)
        • Streaming (2)
        • 기능 개발 (5)
      • 💻 Server (6)
        • Linux (6)
      • ❌ Error Handling (11)
      • 📦 Database (62)
        • SQL (31)
        • NoSQL (2)
        • JPQL (9)
        • QueryDSL (12)
        • Basic (4)
        • Firebase (4)
      • ⚙️ Ops (57)
        • CS (6)
        • AWS (9)
        • Docker (8)
        • Kubernetes (13)
        • MSA (1)
        • CI & CD (20)
      • 📚 Data Architect (48)
        • Data Structure (10)
        • Algorithm (8)
        • Programmers (17)
        • BaekJoon (5)
        • CodeUp (4)
        • Design Pattern (4)
        • AI (0)
      • ⚒️ Management & Tool (8)
        • Git (7)
        • IntelliJ (1)
      • 📄 Document (10)
        • Project 설계 (6)
        • Server Migration (3)
      • 📄 책읽기 (2)
        • 시작하세요! 도커 & 쿠버네티스 (2)
      • 🎮 Game (4)
        • Stardew Vally (1)
        • Path of Exile (3)
  • 블로그 메뉴

    • 링크

      • Github
    • 공지사항

    • 인기 글

    • 태그

      Lock #Thread #Concurrency
      GStreamer #Pipeline
      React #Markdown
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.0
    신건우
    비밀번호 찾기 & 재설정 구현(Google SMTP & Redis)
    상단으로

    티스토리툴바