728x90

개요

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

회원가입 시 받는 이메일 인증은 사용자 식별 및 보안 강화를 위해 필요한 기술이다. 만약 이메일 인증과 같은 인증 기술이 없다면 한 사람이 10개 혹은 1000개의 계정을 무한대로 생성할 수 있다는 것인데, 이는 스팸이나 부정 사용 등 서비스 품질을 하락시킬 수 있다. 이메일 인증과 관련해서 구현하기까지 수많은 구글링과 시행착오가 있어서 정리해놓을려고 한다.

 

나의 환경

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

이메일 인증에 필요한 dto 를 작성한다.

@Getter @Setter
public class MailRequest {

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

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

 

 

2. Service

  • 구현하고자 하는 기능에 맞게 코드를 짠다.
public interface MailService {

    MimeMessage createMail(String mail);

    boolean verifyCode(String email, int code);

    CompletableFuture<Integer> sendMail(String mail);
}
@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 = "메일을 보낼 구글 이메일";
    private static final Map<String, Integer> verificationCodes = new HashMap<>();

    /**
     * 인증 코드 자동 생성 메서드
     */
    public static void createNumber(String email){
        int number = new Random().nextInt(900000) + 100000; // 100000-999999 사이의 숫자 생성
        verificationCodes.put(email, number);
    }

    /**
     * 이메일 전송
     */
    @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));
    }

    /**
     * 이메일 인증 코드 검증
     */
    @Override
    public boolean verifyCode(String mail, int code) {
        Integer storedCode = verificationCodes.get(mail);
        return storedCode != null && storedCode == code;
    }
}

1) 주어진 이메일 주소에 대해 6자리 인증 코드를 생성하고 verificationCodes 맵에 저장한다.
{이메일 : 인증코드} 형태로 저장될 것이다.

2) 입력한 이메일 주소로 발송할 이메일 메시지를 작성한다.

3) 2에서 생성한 이메일 메시지를 비동기적으로 발송한다.

4) 사용자가 입력한 인증코드와 실제 발송된 인증코드와 일치하는지 확인한다.

 

 

 

3. Controller

@RestController
@RequiredArgsConstructor
@EnableAsync
public class MailApiController {

    private final MailService mailService;
    private final UserService userService;

    /**
     * 인증번호 발송 메소드
     */
    @PostMapping("/api/users/mail")
    public CompletableFuture<String> mailSend(@RequestBody MailRequest mailRequest) {
        return mailService.sendMail(mailRequest.getMail())
                .thenApply(number -> String.valueOf(number));
    }

    /**
     * 인증번호 검증 메소드
     */
    @PostMapping("/api/users/verify-code")
    public String verifyCode(@RequestBody MailVerificationRequest verificationRequest) {
        boolean isVerified = mailService.verifyCode(verificationRequest.getMail(), verificationRequest.getCode());
        return isVerified ? "Verified" : "Verification failed";
    }
}
  • 해당 앤드포인트로 요청이 들어오면 해당 요청을 수행한다.

 

4. 프론트엔드

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<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/signup.css">
</head>
<body>
<form id="signup-form" action="/signup" method="post">
    <h1>회원가입</h1>
    <hr/>

    <div th:if="${error}" class="error-message">
        <p th:text="${error}"></p>
    </div>

    <label for="email">이메일</label>
    <input type="email" id="email" name="username" placeholder="이메일 입력" required/>
    <button type="button" id="check-email-button" class="light-button">중복확인</button>
    <span id="emailCheckMessage" class="error"></span>

    <button type="button" id="send-code-button" class="light-button" style="display: none;">인증 코드 발송</button>

    <div id="verifyCodeSection">
        <label for="verificationCode">인증 코드</label>
        <input type="text" id="verificationCode" name="verificationCode" placeholder="인증번호 입력" required/>
        <button type="button" id="verify-code-button" class="light-button">이메일 인증</button>
        <span id="verificationMessage" class="error"></span>
    </div>

    <button type="submit" id="signup-button" disabled>회원가입</button>
</form>
<script src="/js/signup.js"></script>
</body>
</html>
$(document).ready(function() {
    function validateForm() {
        let isValid = true;

        // 이메일, 닉네임 중복 및 비밀번호 일치 여부 검사
        if ($('#emailCheckMessage').hasClass('error')) {
            isValid = false;
        }
        $('#signup-button').prop('disabled', !isValid);
        return isValid;
    }

    // 이메일 중복 검사
    // 1. 이메일 중복되면 인증 메일 보내기 버튼은 숨김
    // 2. 이메일 중복이 없다면 인증 메일 보내기 버튼 활성화 됨
    $('#check-email-button').on('click', function() {
        let email = $('#email').val();

        $.ajax({
            type: 'POST',
            url: '/api/users/check-email',
            contentType: 'application/json',
            data: JSON.stringify({ mail: email }),
            success: function(response) {
                if (response) {
                    $('#emailCheckMessage').text("아이디가 이미 존재합니다.").removeClass('success').addClass('error');
                    $('#send-code-button').hide();  
                } else {
                    $('#emailCheckMessage').text("사용 가능한 아이디입니다.").removeClass('error').addClass('success');
                    $('#send-code-button').show();  
                }
                validateForm();
            },
            error: function(error) {
                $('#emailCheckMessage').text('이메일 확인 중 오류가 발생했습니다. 다시 시도해주세요.').removeClass('success').addClass('error');
                $('#send-code-button').hide();  
                validateForm();
            }
        });
    });

    // 인증 메일
    $('#send-code-button').on('click', function() {
        let email = $('#email').val();

        $.ajax({
            type: 'POST',
            url: '/api/users/mail',
            contentType: 'application/json',
            data: JSON.stringify({ mail: email }),
            success: function(response) {
                $('#verifyCodeSection').show(); 
                alert('인증 메일이 발송되었습니다. 인증 번호를 확인해주세요.');
            },
            error: function(error) {
                alert('메일 발송에 실패했습니다. 다시 시도해주세요.');
            }
        });
    });

    // 인증 코드 확인
    $('#verify-code-button').on('click', function() {
        let email = $('#email').val();
        let code = $('#verificationCode').val();

        $.ajax({
            type: 'POST',
            url: '/api/users/verify-code',
            contentType: 'application/json',
            data: JSON.stringify({ mail: email, code: code }),
            success: function(response) {
                if (response === 'Verified') {
                    $('#verificationMessage').text('인증 성공').removeClass('error').addClass('success');
                } else {
                    $('#verificationMessage').text('인증 실패. 올바른 코드를 입력하세요.').removeClass('success').addClass('error');
                }
            },
            error: function(error) {
                $('#verificationMessage').text('인증 실패. 다시 시도해주세요.').removeClass('success').addClass('error');
            }
        });
    });

    // 인증 코드 발송 버튼 --> 초기 상태에서는 비활성화
    $('#send-code-button').hide();

    // 인증 코드 입력 란 숨기기
    $('#verifyCodeSection').hide();
});
  • 사실 HTML 폼에서 닉네임, 연락처, 생년월일 등 적는 란이 있지만 이메일 인증에 필요한 코드만 남겨두었다.
  1. 이메일이 중복되어 있는지 확인한다. 이 부분은 위에 자바 코드에는 없다.
  2. 이메일이 중복되지 않았다면 해당 이메일로 인증번호를 보낸다.
  3. 인증번호 발송 버튼이 활성화된다. 
// 이메일 중복 검사
    // 1. 이메일 중복되면 인증 메일 보내기 버튼은 숨김
    // 2. 이메일 중복이 없다면 인증 메일 보내기 버튼 활성화 됨
    $('#check-email-button').on('click', function() {
        let email = $('#email').val();

        $.ajax({
            type: 'POST',
            url: '/api/users/check-email',
            contentType: 'application/json',
            data: JSON.stringify({ mail: email }),
            success: function(response) {
                if (response) {
                    $('#emailCheckMessage').text("아이디가 이미 존재합니다.").removeClass('success').addClass('error');
                    $('#send-code-button').hide();  // 오류가 있으면 인증 코드 발송 버튼 숨김
                } else {
                    $('#emailCheckMessage').text("사용 가능한 아이디입니다.").removeClass('error').addClass('success');
                    $('#send-code-button').show();  // 이메일 체크 통과 시 버튼 표시
                }
                validateForm();
            },
            error: function(error) {
                $('#emailCheckMessage').text('이메일 확인 중 오류가 발생했습니다. 다시 시도해주세요.').removeClass('success').addClass('error');
                $('#send-code-button').hide();  // 오류가 있으면 인증 코드 발송 버튼 숨김
                validateForm();
            }
        });
    });

4) 인증코드 입력 섹션이 활성화된다.

// 인증 메일
    $('#send-code-button').on('click', function() {
        let email = $('#email').val();

        $.ajax({
            type: 'POST',
            url: '/api/users/mail',
            contentType: 'application/json',
            data: JSON.stringify({ mail: email }),
            success: function(response) {
                $('#verifyCodeSection').show(); // 인증 코드 입력 섹션 표시
                alert('인증 메일이 발송되었습니다. 인증 번호를 확인해주세요.');
            },
            error: function(error) {
                alert('메일 발송에 실패했습니다. 다시 시도해주세요.');
            }
        });
    });

5) 해당 이메일과 입력한 코드를 /api/users/verify-code 앤드포인트에서 수행한다.

$('#verify-code-button').on('click', function() {
    let email = $('#email').val();
    let code = $('#verificationCode').val();

    $.ajax({
        type: 'POST',
        url: '/api/users/verify-code',
        contentType: 'application/json',
        data: JSON.stringify({ mail: email, code: code }),
        success: function(response) {
            if (response === 'Verified') {
                $('#verificationMessage').text('인증 성공').removeClass('error').addClass('success');
            } else {
                $('#verificationMessage').text('인증 실패. 올바른 코드를 입력하세요.').removeClass('success').addClass('error');
            }
        },
        error: function(error) {
            $('#verificationMessage').text('인증 실패. 다시 시도해주세요.').removeClass('success').addClass('error');
        }
    });
});

 

 

 

5. 결과

 

위에처럼 작성 후 실행하면

해당 이메일로 인증 번호가 전송된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

다음 편은 비밀번호 찾기 로직을 정리할 것이다.

728x90

+ Recent posts