스프링과 JPA 기반 웹 애플리케이션 개발 #32 패스워드 수정

Jake Seo·2021년 6월 4일
0

스프링과 JPA 기반 웹 애플리케이션 개발 #32 패스워드 수정

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

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

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


패스워드 수정

  • 패스워드 탭 활성화
  • 새 패스워드와 새 패스워드 확인 값이 일치해야 한다.
  • 패스워드 인코딩
  • 둘 다 최소 8자에서 최대 50자 사이
  • 사용자 정보를 변경하는 작업
    • 서비스로 위임해서 트랜잭션 안에서 처리해야 한다.
    • 또는 Detached 상태의 객체를 변경한 다음 Repository.save()를 호출해서 상태 변경 내용을 적용해도 된다.

PasswordForm 클래스 작성

@Data
public class PasswordForm {
    @Length(min = 8, max = 50)
    private String newPassword;
    @Length(min = 8, max = 50)
    private String newPasswordConfirm;
}

PasswordFormValidator 클래스 작성

@Component
public class PasswordFormValidator implements Validator {
    @Override
    public boolean supports(Class<?> aClass) {
        // 매개변수로 해당 클래스/인터페이스를 상속/구현한 클래스가 들어온건지 확인
        // Object 의 instanceOf 와 비슷하다.
        return PasswordForm.class.isAssignableFrom(aClass);
    }

    @Override
    public void validate(Object o, Errors errors) {
        PasswordForm passwordForm = (PasswordForm) o;
        if(!passwordForm.getNewPassword().equals(passwordForm.getNewPasswordConfirm())) {
            errors.rejectValue("newPassword","wrong.value", "입력한 새 패스워드가 일치하지 않습니다.");
        }
    }
}

SettingsController에 PasswordFormValidator 적용

...
public class SettingsController {
    private final AccountService accountService;
    private final PasswordFormValidator passwordFormValidator;
...

@InitBinder("passwordForm")
    // WebDataBinder 는 WebRequestParameters 를 받아서 Java Beans Object 형태로 변환해주는 역할을 한다.
    // 여기에 .addValidator() 를 사용하면 특정 데이터가 들어왔을 때, 필터를 걸어 검증하는 역할이다.
    // 특정 데이터를 판독하는 기준은 이전에 우리가 .supports() 메소드에 코딩했던 것과 같다.
    // PasswordForm.class.isAssignableFrom(class) 로 해당 클래스/인터페이스를 구현/상속했는지 확인하고 검증에 들어간다.
    public void initBinder(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(passwordFormValidator);
    }

WebDataBinder가 클라이언트로부터 WebRequestParameters 데이터를 받아서 자바 객체 (Java Beans Object)로 매핑을 한 이후에 .addValidators()에 등록된 Validator로 검증을 한다.

이전에 PasswordFormValidator에서 작성했던 .support()WebDataBinder에서 매핑한 클래스가 PasswordForm에 할당 가능한 것(PasswordForm 클래스이거나, PasswordForm 클래스를 상속했거나)이면, 해당 검증을 지원한다고 한 것이다.

아래 .validate()의 내용은 실제 검증 내용인데, 넘겨받은 객체를 PasswordForm 형태로 캐스팅한다. 넘겨받는 객체는 PasswordForm 타입으로 변환 가능할 수 밖에 없는데, 그 이유는 우리가 .support()메소드에서 PasswordForm.class.isAssignableFrom()으로 한번 검증을 했기 때문이다.

어찌됐든 PasswordForm 형태로 캐스팅하여, 해당 객체에 들어있는 두 패스워드가 일치하는지 검증한다.

기타 GET & POST 메소드들

    @GetMapping(PASSWORD_MAPPING_PATH)
    public String updatePasswordForm(@LoginAccount Account loginAccount,
                                     Model model) {
        model.addAttribute(loginAccount);
        model.addAttribute(new PasswordForm());

        return PASSWORD_MAPPING_PATH;
    }

    @PostMapping(PASSWORD_MAPPING_PATH)
    public String updatePassword(
            // Model 맨 뒤로 빼야한다.
            // 왜냐하면 Attribute 들이 매핑돼서 결국 Model에 들어가야 하기 때문
            @LoginAccount Account loginAccount,
            @Valid @ModelAttribute PasswordForm passwordForm,
            Errors errors,
            RedirectAttributes redirectAttributes,
            Model model
    ) {
        if(errors.hasErrors()) {
           model.addAttribute(loginAccount);
           return PASSWORD_MAPPING_PATH;
        }

        accountService.updatePassword(loginAccount, passwordForm.getNewPassword());
        redirectAttributes.addFlashAttribute("message", "패스워드를 변경했습니다.");

        return "redirect:/" + PASSWORD_MAPPING_PATH;

위의 내용은 전에 배웠던 내용들과 같다. 단, 파라미터에서 Model이 맨 뒤로 가는 이유는 항상 인지하자. ModelAttribute 등에서 적용된 내용을 Model로 자동 매핑하여 옮겨줘야 하기 때문에, Model이 맨 뒤로 가있지 않으면 400에러를 반환한다. 조심하자.

또한 패스워드 변경 부분은 데이터의 변화가 일어나는 부분이므로 accountService에게 역할을 위임했다.

AccountService

updatePassword

    public void updatePassword(Account account, String newPassword) {
        account.setPassword(passwordEncoder.encode(newPassword));
    }

패스워드를 넣을 때는 항상 인코딩 해서 넣는다.

password.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="alert alert-warning" role="alert" th:if="${account != null && !account?.emailVerified}">
    스터디올래 가입을 완료하려면 <a href="#" th:href="@{/check-email}" class="alert-link">계정 인증 이메일을 확인</a>하세요
</div>

<div class="container">
    <!-- row의 기본은 col-12만큼의 크기를 갖는다.-->
    <div class="row mt-5 justify-content-center">
        <div class="col-2">
            <div th:replace="fragments :: settings-menu(currentMenu='profile')"></div>
        </div>
        <div class="col-8">
            <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 class="row">
                <h2 class="col-sm-12">패스워드 변경</h2>
            </div>
            <div class="row mt-3">
                <form class="needs-validation col-12" action="#" th:action="@{/settings/password}"
                      th:object="${passwordForm}" method="post" novalidate>
                    <div class="form-group">
                        <label for="newPassword">새 패스워드</label>
                        <input id="newPassword" type="password" th:field="*{newPassword}"
                               class="form-control" aria-describedby="newPasswordHelp" required min="8" max="50">
                        <small id="newPasswordHelp" class="form-text text-muted">
                            새 패스워드를 입력하세요.
                        </small>
                        <small class="invalid-feedback">패스워드를 입력하세요.</small>
                        <small class="form-text text-danger" th:if="${#fields.hasErrors('newPassword')}"
                               th:errors="*{newPassword}">
                            패스워드 너무 길어요.
                        </small>
                    </div>

                    <div class="form-group">
                        <label for="newPasswordConfirm">새 패스워드</label>
                        <input id="newPasswordConfirm" type="password" th:field="*{newPasswordConfirm}"
                               class="form-control" aria-describedby="newPasswordConfirmHelp" required min="8" max="50">
                        <small id="newPasswordConfirmHelp" class="form-text text-muted">
                            새 패스워드를 입력하세요.
                        </small>
                        <small class="invalid-feedback">패스워드를 입력하세요.</small>
                        <small class="form-text text-danger" th:if="${#fields.hasErrors('newPasswordConfirm')}"
                               th:errors="*{newPasswordConfirm}">
                            패스워드 너무 길어요.
                        </small>
                    </div>

                    <div class="form-group">
                        <button class="btn btn-primary btn-block" type="submit"
                                aria-describedby="submitHelp">수정하기
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
    <th:block th:replace="fragments :: footer"></th:block>
</div>

<script th:replace="fragments :: form-validation"></script>
</body>
</html>

위와 같이 모든 공간을 다 쓸때는 col-sm-12 와 같은 클래스명에서 sm과 같이 해상도에 관련된 부분은 안넣어도 된다는 것을 인지하자.

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

0개의 댓글