스프링과 JPA 기반 웹 애플리케이션 개발 #37 패스워드를 잊어버렸습니다. (+스프링 MVC의 폼처리 패턴 복습)

Jake Seo·2021년 6월 8일
0

스프링과 JPA 기반 웹 애플리케이션 개발 #37 패스워드를 잊어버렸습니다.

해당 내용은 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발의 강의 내용을 바탕으로 작성된 내용입니다.

강의를 학습하며 요약한 내용을 출처를 표기하고 블로깅 또는 문서로 공개하는 것을 허용합니다 라는 원칙 하에 요약 내용을 공개합니다. 출처는 위에 언급되어있듯, 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발입니다.

제가 학습한 소스코드는 https://github.com/n00nietzsche/jakestudy_webapp 에 지속적으로 업로드 됩니다. 매 커밋 메세지에 강의의 어디 부분까지 진행됐는지 기록해놓겠습니다.


패스워드를 잊어버렸습니다.

  • 패스워드를 잊은 경우에는 "로그인 할 수 있는 링크"를 이메일로 전송한다. 이메일로 전송된 링크를 클릭하면 로그인한다.
  • GET /email-login
    • 이메일을 입력할 수 있는 폼을 보여주고, 링크 전송 버튼을 제공한다.
  • POST /email-login
    • 입력받은 이메일에 해당하는 계정을 찾아보고, 있는 계정이면 로그인 가능한 링크를 이메일로 전송한다.
    • 이메일 전송 후, 안내 메세지를 보여준다.
  • GET /login-by-email
    • 토큰과 이메일을 확인한 뒤 해당 계정으로 로그인한다.

1.Form Backing Object 작성

@Data
public class EmailForm {
    @Email
    @NotBlank
    private String email;
}

javax.validation을 이용하여 기본적인 검증을 해주는 곳이며, 나중에 ModelMapper 혹은 그냥 setter를 통해 도메인 객체에 합쳐질 정보를 보관하는 곳이다.

2. Validator 작성 (Optional)

@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 객체를 전달해 검증 실패 시 로직을 작성하기 편리하게 만들어준다.

3. 컨트롤러 작성

3.1. initBinder() 작성

    @InitBinder("emailForm")
    public void initBinderEmailForm(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(emailFormValidator);
    }

Validator를 작성했으면 등록해준다.

3.2. GPRG 및 비즈니스 로직 작성

    @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하여 반환하면, 새로고침 시에 같은 내용이 폼에 다시 전달되어 또 같은 내용을 전달하게 되니까 주의하자.

4. 서비스쪽 메소드 구현

    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를 반환한다.

5. HTML 작성

<!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>
profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글