개요
멋사 파이널 프로젝트 중 회원가입 시 이메일 인증과 비밀번호 찾기 로직을 구현해보고자 한다.
회원가입 시 받는 이메일 인증은 사용자 식별 및 보안 강화를 위해 필요한 기술이다. 만약 이메일 인증과 같은 인증 기술이 없다면 한 사람이 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)
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. 이메일 중복이 없다면 인증 메일 보내기 버튼 활성화 됨
$('#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. 결과
위에처럼 작성 후 실행하면
해당 이메일로 인증 번호가 전송된다.
다음 편은 비밀번호 찾기 로직을 정리할 것이다.
'Spring > Spring Boot' 카테고리의 다른 글
Docker 및 Redis 설치/설정 (0) | 2024.08.22 |
---|---|
Spring 이메일 인증 기능 구현하기 2 : 비밀번호 찾기 (0) | 2024.08.20 |
SpringJPA : 영속성 컨텍스트, 엔티티 매핑, @OneToMany, @ManyToOne (1) | 2024.06.21 |
DB - JOIN, SUBQUERY (0) | 2024.05.03 |
추상클래스, 인터페이스 (0) | 2024.04.11 |