728x90

개요

멋사 파이널 프로젝트 중 회원가입 시 이메일 인증과 비밀번호 찾기 로직을 구현해보고자 한다.

회원가입에 성공 후 비밀번호를 잊어버렸을 시 코드 구현을 하고자 한다.

 

나의 환경

Window 11

intelliJ

java 21 

spring Boot 3.3.0

spring Security 6 

jwt 0.11.5

 

의존성 설치

// email smtp
implementation 'org.springframework.boot:spring-boot-starter-mail'

 

필요한 환경변수 세팅 Google SMTP

  • 비밀번호는 구글 이메일의 비밀번호가 아니라 구글 설정에서 앱 비밀번호를 생성받아야 한다.
  • 아래 링크에서 잘 설명해주셔서 참고!
spring:
  application:
    name: OMG_project
  mail:
    host: smtp.gmail.com
    port: 587
    username: {gmail email}
    password: {password}
    properties:
      mail:
        smtp:
          starttls:
            enable: true
          auth: true

[Go] Google Gmail SMTP 설정 방법 및 메일 전송 (tistory.com)

 

[Go] Google Gmail SMTP 설정 방법 및 메일 전송

■ SMTP 간이 우편 전송 프로토콜(Simple Mail Transfer Protocol)의 약자. 이메일 전송에 사용되는 네트워크 프로토콜이다. 인터넷에서 메일 전송에 사용되는 표준이다. 1982년 RFC821에서 표준화되어 현재

hyunmin1906.tistory.com

 

 

1. dto

@Getter @Setter
public class MailRequest {

    private String mail;
}
@Getter @Setter
public class PasswordVerificationRequest {

    private String mail;
    private String tempPassword;
}
  • 사실 역할로 봤을 때는 dto의 기능을 하지 않기 때문에 dto라 명명한 것이 잘못되었지만... 나중에 수정하는 것으로 하고 넘어갔다.

 

2. Service

public interface MailService {

    String createTemporaryPassword(String email);

    boolean verifyTemporaryPassword(String email, String tempPassword);

    void sendTemporaryPasswordMail(String email, String tempPassword);
}
@Service
@RequiredArgsConstructor
@Slf4j
public class MailServiceImpl implements MailService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JavaMailSender javaMailSender;
    private static final String senderEmail = "ch9800113@gmail.com";
    private static final Map<String, Integer> verificationCodes = new HashMap<>();

    /**
     * 임시 비밀번호 자동 생성 메서드
     */
    private static String generateRandomPassword() {
        int length = 8;
        StringBuilder sb = new StringBuilder(length);
        Random random = new Random();
        for (int i = 0; i < length; i++) {
            sb.append((char) (random.nextInt(10) + '0'));
        }
        return sb.toString();
    }

    /**
     * 임시 비밀번호 전송
     */
    @Override
    public void sendTemporaryPasswordMail(String mail, String tempPassword) {
        MimeMessage message = javaMailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            helper.setFrom(senderEmail);
            helper.setTo(mail);
            helper.setSubject("OMG 임시 비밀번호");
            String body = "<h2>OMG에 오신걸 환영합니다!</h2><p>아래의 임시 비밀번호를 사용하세요.</p><h1>" + tempPassword + "</h1><h3>반드시 비밀번호를 재설정하세요.</h3>";
            helper.setText(body, true);
            javaMailSender.send(message);
        } catch (MessagingException e) {
            throw new RuntimeException("임시 비밀번호 전송 오류", e);
        }
    }

    /**
     * 임시 비밀번호 생성 및 DB 업데이트
     */
    @Override
    public String createTemporaryPassword(String mail) {
        String tempPassword = generateRandomPassword();
        User user = userRepository.findByUsername(mail)
                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
        user.setPassword(passwordEncoder.encode(tempPassword));
        userRepository.save(user);
        return tempPassword;
    }

    /**
     * 임시 비밀번호 검증
     */
    @Override
    public boolean verifyTemporaryPassword(String mail, String tempPassword) {
        User user = userRepository.findByUsername(mail)
                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
        return passwordEncoder.matches(tempPassword, user.getPassword());
    }
}

 

 

[ 코드 설명 ]

/**
 * 임시 비밀번호 자동 생성 메서드
 */
private static String generateRandomPassword() {
    int length = 8;
    StringBuilder sb = new StringBuilder(length);
    Random random = new Random();
    for (int i = 0; i < length; i++) {
        sb.append((char) (random.nextInt(10) + '0'));
    }
    return sb.toString();
}
  • 반환 값: String (문자열)
  • 생성 방식: 이 메서드는 8자리의 숫자로 구성된 문자열을 생성하는 메서드이다.
    • 문자열은 StringBuilder를 사용하여 효율적으로 생성되도록 구현했다.
    • 각 반복에서 random.nextInt(10) + '0'을 통해 0부터 9까지의 숫자를 문자로 변환하여 문자열에 추가한다.
  • StringBuilder 사용이유 ::
    • String은 불변 객체(immutable object)이다. 즉 한 번 생성된 String은 변경할 수 없으며, 문자열의 조작은 새로운 String 객체를 생성하여 처리된다.
    • StringBuilder를 사용하여 문자열을 생성한 후, 최종적으로 toString() 메서드를 호출하여 불변의 String 객체를 반환하도록 구현했다.
    • 위의 코드는 숫자로만 구성했지만, 나중에 보안을 위해 아래처럼 작성하는 것으로 바꾸었다.
  •  
private static String generateRandomPassword() {
    int length = 8;
    StringBuilder sb = new StringBuilder(length);
    Random random = new Random();
    String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    for (int i = 0; i < length; i++) {
        sb.append(characters.charAt(random.nextInt(characters.length())));
    }

    return sb.toString();
}
/**
 * 임시 비밀번호 전송
 */
@Override
public void sendTemporaryPasswordMail(String mail, String tempPassword) {
    MimeMessage message = javaMailSender.createMimeMessage();
    try {
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
        helper.setFrom(senderEmail);
        helper.setTo(mail);
        helper.setSubject("임시 비밀번호");
        String body = "<h2>000에 오신걸 환영합니다!</h2><p>아래의 임시 비밀번호를 사용하세요.</p><h1>" + tempPassword + "</h1><h3>반드시 비밀번호를 재설정하세요.</h3>";
        helper.setText(body, true);
        javaMailSender.send(message);
    } catch (MessagingException e) {
        throw new RuntimeException("임시 비밀번호 전송 오류", e);
    }
}
  • 반환 값: void
  •  생성 방식 : 이 메서드는 임시비밀번호를 이메일로 전송하는 기능만 수행하고, 결과를 반환할 필요가 없다
    • javaMailSender.send(message); 를 통해 메서드에서 바로 구현하여 바로 메일을 전송하였다.
  • 하지만 저번 포스트에서 회원가입시 이메일 인증 번호 전송 로직을 보면
@Override
public MimeMessage createMail(String mail){
    createNumber(mail);
    MimeMessage message = javaMailSender.createMimeMessage();

    try {
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
        helper.setFrom(senderEmail);
        helper.setTo(mail);
        helper.setSubject("이메일 인증번호");
        String body = "<h2>000에 오신걸 환영합니다!</h2><h3>아래의 인증번호를 입력하세요.</h3><h1>" + verificationCodes.get(mail) + "</h1><h3>감사합니다.</h3>";
        helper.setText(body, true);
    } catch (MessagingException e) {
        e.printStackTrace();
    }

    return message;
}

/**
 * createMail() 메서드의 내용을 이메일 전송
 */
@Async
@Override
public CompletableFuture<Integer> sendMail(String mail) {
    MimeMessage message = createMail(mail);
    javaMailSender.send(message);
    return CompletableFuture.completedFuture(verificationCodes.get(mail));
}
  • 여기서 코드의 반환값은 MimeMessage 이다.
  • 즉 javaMailSender.send(message); 를 하지 않았기 때문에 따로 sendMail 메서드를 통해 이메일 전송을 해줘야 한다.
  • MimeMessage  란? ::
    • MimeMessage  객체는 이메일 메시지를 생성하고 설정하는 데 사용되는 객체이다.
    • 발신자, 수신자, 제목, 본문 내용, 첨부 파일 등 이메일의 모든 구성 요소를 설정할 수 있고 설정된 MimeMessage 객체는 JavaMailSender를 통해 이메일 서버로 전송해야 한다.
  • 나중에 코드 통일성을 위해 하나의 메서드에서 전송될 수 있도록 구현할 예정이다.

 

 

3. Controller

@RestController
@RequiredArgsConstructor
@EnableAsync
public class MailApiController {

    private final MailService mailService;
    private final UserService userService;

    /**
     * 임시 비밀번호 재발급 발송 메서드
     */
    @PostMapping("/api/users/reset-password")
    public ResponseEntity<String> resetPassword(@RequestBody MailRequest mailRequest) {
        String email = mailRequest.getMail();

        if (userService.existsByUsername(email)) {
            String tempPassword = mailService.createTemporaryPassword(email);
            mailService.sendTemporaryPasswordMail(email, tempPassword);
            return ResponseEntity.ok("임시 비밀번호가 이메일로 발송되었습니다.");
        } else {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("해당 이메일로 가입된 사용자가 없습니다.");
        }
    }

    /**
     * 임시 비밀번호 검증 메소드
     */
    @PostMapping("/api/users/verify-temporary-password")
    public ResponseEntity<String> verifyTemporaryPassword(@RequestBody PasswordVerificationRequest request) {
        boolean isVerified = mailService.verifyTemporaryPassword(request.getMail(), request.getTempPassword());
        return isVerified ? ResponseEntity.ok("Verified") : ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Verification failed");
    }
}
/**
 * 비밀번호 재발급 페이지 이동
 */
@GetMapping("/users/reset-user-password")
public String showResetPasswordForm() {
    return "/user/findPassword";
}

 

 

4. 프론트엔드

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>비밀번호 재발급</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <link rel="stylesheet" href="/css/find-password.css">
</head>
<body>
<form id="emailVerificationForm">

    <h1>비밀번호 재발급</h1>
    <label for="email">가입 이메일</label>
    <input type="email" id="email" name="email" placeholder="Email" required/>
    <button type="button" id="send-code-button">임시 비밀번호 발송</button>
    <span id="emailCheckMessage" class="error"></span>

    <div id="verifyCodeSection">
        <label for="temporaryPassword">임시 비밀번호</label>
        <input type="text" id="temporaryPassword" name="temporaryPassword" placeholder="임시 비밀번호 입력" required />
        <button type="button" id="verify-temporary-password-button">임시 비밀번호 확인</button>
        <span id="verificationMessage" class="error"></span>
    </div>
</form>
<script>
    $(document).ready(function() {
        $('#verifyCodeSection').hide();

        $('#send-code-button').on('click', function() {
            let email = $('#email').val();
            $.ajax({ // 이메일이 데이터베이스에 있는지 확인
                url: '/api/users/check-email',
                type: 'POST',
                contentType: 'application/json',
                data: JSON.stringify({ mail: email }),
                success: function(response) {
                    if (response) {
                        $.ajax({ // 이메일이 존재하면 임시 비밀번호 발송
                            url: '/api/users/reset-password',
                            type: 'POST',
                            contentType: 'application/json',
                            data: JSON.stringify({ mail: email }),
                            success: function(response) {
                                $('#verifyCodeSection').slideDown(); // 인증 코드 입력 섹션 표시
                                alert('임시 비밀번호가 이메일로 발송되었습니다. 이메일을 확인해주세요.');
                            },
                            error: function(error) {
                                alert('임시 비밀번호 발송에 실패했습니다. 다시 시도해주세요.');
                            }
                        });
                    } else {
                        $('#emailCheckMessage').text('해당 이메일로 가입된 사용자가 없습니다.').show();
                    }
                },
                error: function(error) {
                    alert('이메일 확인 중 오류가 발생했습니다. 다시 시도해주세요.');
                }
            });
        });

        $('#verify-temporary-password-button').on('click', function() {
            const email = $('#email').val();
            const tempPassword = $('#temporaryPassword').val();
            $.ajax({
                url: '/api/users/verify-temporary-password',
                type: 'POST',
                contentType: 'application/json',
                data: JSON.stringify({ mail: email, tempPassword: tempPassword }),
                success: function(response) {
                    if (response === "Verified") {
                        alert("임시 비밀번호가 확인되었습니다. 로그인하세요.");
                        window.location.href = "/signin";
                    } else {
                        $('#verificationMessage').text("임시 비밀번호가 일치하지 않습니다. 다시 시도하세요.").show();
                    }
                },
                error: function(xhr, status, error) {
                    alert("임시 비밀번호 검증에 실패했습니다. 다시 시도하세요.");
                }
            });
        });
    });
</script>
</body>
</html>
  1. 해당 이메일이 디비에 있는지 확인한다.
  2. 이메일이 존재한다면 해당 이메일로 임시 비밀번호를 전송해준다.
  3. 이때 폼에서는 숨겼던 섹션을 표시하고 임시비밀번호를 입력 란을 활성화한다.
  4. 임시비밀번호가 이메일로 전송한 값과 같은지 확인한다.
  5. 이때 임시비밀번호가 디비에 업데이트가 되어서 기존 비밀번호로 인증을 할 수 없게 된다.

 

여기까지 끝-!

 

 

 

 

 


jQuery에서 HTML 요소를 표시하는 두 가지 방법

 

  • jQuery 에서 아래의 두가지가 섹션을 보여주는 방식의 차이라서 정리한다.
// 애니메이션 효과로 서서히 표시
$('#verifyCodeSection').slideDown();

// 즉시 표시
$('#verifyCodeSection').show();

 

 

 

728x90

+ Recent posts