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>
- 해당 이메일이 디비에 있는지 확인한다.
- 이메일이 존재한다면 해당 이메일로 임시 비밀번호를 전송해준다.
- 이때 폼에서는 숨겼던 섹션을 표시하고 임시비밀번호를 입력 란을 활성화한다.
- 임시비밀번호가 이메일로 전송한 값과 같은지 확인한다.
- 이때 임시비밀번호가 디비에 업데이트가 되어서 기존 비밀번호로 인증을 할 수 없게 된다.
여기까지 끝-!
jQuery에서 HTML 요소를 표시하는 두 가지 방법
- jQuery 에서 아래의 두가지가 섹션을 보여주는 방식의 차이라서 정리한다.
// 애니메이션 효과로 서서히 표시
$('#verifyCodeSection').slideDown();
// 즉시 표시
$('#verifyCodeSection').show();
728x90
'Spring > Spring Boot' 카테고리의 다른 글
Spring : S3 이미지 업로드 구현 (0) | 2024.08.23 |
---|---|
Docker 및 Redis 설치/설정 (0) | 2024.08.22 |
Spring 이메일 인증 기능 구현하기 1편 : 회원가입시 이메일 인증 (0) | 2024.08.20 |
SpringJPA : 영속성 컨텍스트, 엔티티 매핑, @OneToMany, @ManyToOne (1) | 2024.06.21 |
DB - JOIN, SUBQUERY (0) | 2024.05.03 |