스프링과 JPA 기반 웹 애플리케이션 개발 #37 패스워드를 잊어버렸습니다.
해당 내용은 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발의 강의 내용을 바탕으로 작성된 내용입니다.
강의를 학습하며 요약한 내용을 출처를 표기하고 블로깅 또는 문서로 공개하는 것을 허용합니다 라는 원칙 하에 요약 내용을 공개합니다. 출처는 위에 언급되어있듯, 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발입니다.
제가 학습한 소스코드는 https://github.com/n00nietzsche/jakestudy_webapp 에 지속적으로 업로드 됩니다. 매 커밋 메세지에 강의의 어디 부분까지 진행됐는지 기록해놓겠습니다.
GET /email-login
POST /email-login
GET /login-by-email
@Data
public class EmailForm {
@Email
@NotBlank
private String email;
}
javax.validation
을 이용하여 기본적인 검증을 해주는 곳이며, 나중에 ModelMapper
혹은 그냥 setter를 통해 도메인 객체에 합쳐질 정보를 보관하는 곳이다.
@Component
@RequiredArgsConstructor
public class EmailFormValidator implements Validator {
private final AccountRepository accountRepository;
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(EmailForm.class);
}
@Override
public void validate(Object target, Errors errors) {
EmailForm emailForm = (EmailForm) target;
if(!accountRepository.existsByEmail(emailForm.getEmail())) {
errors.rejectValue("email", "not.exist.email", new Object[]{emailForm.getEmail()}, "존재하지 않는 이메일 입니다.");
}
}
}
리포지토리 등 빈을 통한 검증이 필요할 때 주로 사용하는 Validator
이다. 검증 실패 시에 컨트롤러에 Errors
객체를 전달해 검증 실패 시 로직을 작성하기 편리하게 만들어준다.
@InitBinder("emailForm")
public void initBinderEmailForm(WebDataBinder webDataBinder) {
webDataBinder.addValidators(emailFormValidator);
}
Validator
를 작성했으면 등록해준다.
@GetMapping("/login-by-email")
public String loginByEmailForm(Model model) {
model.addAttribute(new EmailForm());
return "/login-by-email";
}
@PostMapping("/login-by-email")
public String loginByEmail(@Valid @ModelAttribute EmailForm emailForm, Errors errors, RedirectAttributes redirectAttributes, Model model) {
if(errors.hasErrors()) {
// model.addAttribute("error", "에러 발생");
return "/login-by-email";
}
if(accountService.sendLoginConfirmEmail(emailForm.getEmail())) {
redirectAttributes.addFlashAttribute("message", "이메일이 전송되었습니다. 이메일을 확인해주세요.");
}else {
redirectAttributes.addFlashAttribute("error", "이미 이메일을 재전송했습니다. 1시간 이내에 이메일을 재전송 할 수 없습니다.");
}
return "redirect:/login-by-email";
}
@GetMapping("/check-login-token")
public String checkLoginToken(String token, String email, RedirectAttributes redirectAttributes, Model model) {
Account account = accountRepository.findByEmail(email);
if (account == null || !account.isValidLoginCheckToken(token)) {
redirectAttributes.addFlashAttribute("error", "토큰 혹은 이메일이 잘못되었습니다.");
return "redirect:/login-by-email";
}
accountService.login(account);
return "redirect:/";
}
실패 시 redirect:/
를 적극 활용하자. /login-by-email
과 같은 경로를 사용할 때, 폼에 넣어줄 ModelAttribute
의 정보가 없다면, th:object
와 같은 thymeleaf 기능을 썼을 때 에러가 나니까, 항상 유의하자. redirect:/
를 이용하면 이러한 현상을 방지하기 쉽다.
또한 POST
메소드는 redirect:/
로 뷰를 반환하지 않고, 그냥 return
하여 반환하면, 새로고침 시에 같은 내용이 폼에 다시 전달되어 또 같은 내용을 전달하게 되니까 주의하자.
public boolean sendLoginConfirmEmail(String email) {
Account account = accountRepository.findByEmail(email);
if(account.canSendLoginCheckTokenAgain()) {
account.generateLoginCheckToken();
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setTo(account.getEmail());
simpleMailMessage.setSubject("제이크 스터디 이메일로 로그인");
simpleMailMessage.setText("/check-login-token?token=" + account.getLoginCheckToken() + "&email=" + account.getEmail());
javaMailSender.send(simpleMailMessage);
return true;
} else {
return false;
}
로그인 토큰 확인 이메일을 보내는 부분이다. 로그인 이메일을 다시 보낼 수 있는지 확인하는 .canSendLoginCheckTokenAgain()
을 이용하여 이메일을 성공적으로 보내면 true
못보내면 false
를 반환한다.
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>로그인</title>
<th:block th:replace="fragments :: headLibraryInjection"></th:block>
</head>
<body class="bg-light">
<th:block th:replace="fragments :: main-nav"></th:block>
<div class="container">
<div class="py-5 text-center">
<p class="lead">스터디올래</p>
<h2>로그인</h2>
</div>
<div class="row justify-content-center">
<div th:if="${param.error}" class="alert alert-danger" role="alert">
<p>이메일이 정확하지 않습니다.</p>
</div>
<div th:if="${message}" class="alert alert-info alert-dismissible fade show mt-3" role="alert">
<span th:text="${message}">알림 메시지</span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">x</span>
</button>
</div>
<div th:if="${error}" class="alert alert-danger alert-dismissible fade show mt-3" role="alert">
<span th:text="${error}">에러 메시지</span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">x</span>
</button>
</div>
<!-- `th:object` 를 쓴 경우, 사용할 객체를 올바르게 모델에서 내려받지 못한 경우 500에러가 나니까 조심해야 한다. -->
<form class="needs-validation col-sm-6" th:object="${emailForm}" action="#" th:action="@{/login-by-email}" method="post" novalidate>
<div class="form-group">
<label for="username">이메일</label>
<input id="username" type="text" th:field="*{email}" class="form-control"
placeholder="your@email.com" aria-describedby="emailHelp" required />
<small id="emailHelp" class="form-text text-muted">
가입할 때 사용한 이메일을 입력하세요.
</small>
<small class="invalid-feedback">이메일을 입력하세요.</small>
<!-- `th:if`는 `Validator` 에서 `.rejectValue()` 에 넣은 `field` 값을 인식하여 에러가 있는지 확인한다. -->
<small class="form-text text-danger" th:if="${#fields.hasErrors('email')}" th:errors="*{email}">
이메일이 잘못되었습니다.
</small>
</div>
<div class="form-group">
<button class="btn btn-success btn-block" type="submit"
aria-describedby="submitHelp">로그인</button>
<small class="form-text text-muted">스터디올래에 처음 오셨다면,
<a href="#" th:href="@{/sign-up}">계정을 먼저 만드세요.</a>
</small>
</div>
</form>
</div>
<th:block th:replace="fragments :: footer"></th:block>
</div>
<script th:replace="fragments :: form-validation"></script>
</body>
</html>