27. 패스워드 수정
1. 패스워드 탭 활성화
2. 새 패스워드와 새 패스워드 확인 값 일치
3. 패스워드 인코딩
4. 둘다 최소 8~50자 사이
5. 사용자 정보를 변경하는 작업
> 서비스로 위임해서 트랜잭션 안에서 처리해야 하낟.
> 또는 Detached 상태의 객체를 변경한 다음 Repository의 save를 호출해서 상태 변경 내역을 적용할 것(Merge)
백엔드 로직 처리하기
1. 첫 번째로는 PasswordForm을 생성 (variable : newPassowrd, newPasswordConfirm)
@Data
public class PasswordForm {
@Length(min = 8, max =50)
private String newPassword;
@Length(min = 8, max =50)
private String newPasswordConfirm;
}
2. Password와 PasswordConfirm값이 일치하는지 확인해주는 PasswordFormValidator를 생성
public class PasswordFormValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return PasswordForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
PasswordForm passwordForm = (PasswordForm) target;
if(!passwordForm.getNewPassword().equals(passwordForm.getNewPasswordConfirm())){
errors.rejectValue("newPassword", "wrong value", "입력한 새 패스워드가 일치하지 않습니다.");
}
}
}
Controller
3. SettingsController에서 비밀번호 변경 로직 생성
Front단에서 설정한 비밀번호 변경 url
<a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'password'}? active" href="#" th:href="@{/settings/password}">패스워드</a>
SettingsController.java
@Controller
@RequiredArgsConstructor
public class SettingsController {
/**
* AccountController - SignUpForm - SignUpFormValidator와 같은 구조로 동작
* passwordForm 데이터를 받을때 바인더를 설정
* passwordFormValidator는 Bean등록을 안했기 때문에 new로 객체 생성
**/
@InitBinder("passwordForm")
public void initBinder(WebDataBinder webDataBinder){
webDataBinder.addValidators(new PasswordFormValidator());
}
static final String SETTINGS_PROFILE_VIEW_NAME = "settings/profile";
static final String SETTINGS_PROFILE_URL = "/settings/profile";
// ...
// Password 변경 페이지
@GetMapping(SETTINGS_PASSWORD_URL)
public String updatePasswordForm(@CurrentUser Account account, Model model) {
model.addAttribute(account);
model.addAttribute(new PasswordForm());
return SETTINGS_PASSWORD_VIEW_NAME;
}
@PostMapping(SETTINGS_PASSWORD_URL)
public String updatePassword(@CurrentUser Account account, @Valid PasswordForm passwordForm, Errors errors,
Model model, RedirectAttributes attributes){
// 에러
if(errors.hasErrors()){
model.addAttribute(account);
return SETTINGS_PASSWORD_VIEW_NAME;
}
// 비밀번호 변경 성공
accountService.updatePassword(account, passwordForm.getNewPassword());
attributes.addFlashAttribute("message", "패스워드를 변경했습니다.");
return "redirect:" + SETTINGS_PASSWORD_URL;
}
}
4. Service단에서 updatePassword로직 생성
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AccountService implements UserDetailsService {
private final PasswordEncoder passwordEncoder;
// ...
public void updatePassword(Account account, String newPassword) {
account.setPassword(passwordEncoder.encode(newPassword));
accountRepository.save(account); // detached인 상태인 객체(Account)를 명시적으로 merge
}
}
Front 개발
5. /settings/password.html 생성
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments.html :: head"></head>
<body class="bg-light">
<div th:replace="fragments.html :: main-nav"></div>
<div class="container">
<div class="row mt-5 justify-content-center">
<div class="col-2">
<div th:replace="fragments.html :: settings-menu(currentMenu='password')"></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">×</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}">New Password Error</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}">New Password Confirm Error</small>
</div>
<div class="form-group">
<button class="btn btn-outline-primary" type="submit" aria-describedby="submitHelp">패스워드 변경하기</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script th:replace="fragments.html :: form-validation"></script>
</body>
</html>
그러면 아래와 같이 Form이 생성된다
결과화면 테스트
마지막으로 로직이 잘 먹히는지 테스트를 해보자
1. newPassword != newPasswordConfirm
2. 비밀번호 길이 8~50자 사이로 입력
3. 에러없이 비밀번호 변경 완료
테스트 코드 작성
테스트코드는 3가지 케이스로 나눠서 테스트를 하였다.
- 패스워드 수정 폼이 제대로 나오는지
- 패스워드가 정상적으로 수정이 되는지
- 패스워드 불일치 시 에러가 정상적으로 발생하는지
SettingsControllerTest.java
// 패스워드 수정 테스트
@WithAccount("loosie")
@DisplayName("패스워드 수정 폼")
@Test
void updatePassword_form() throws Exception {
mockMvc.perform(get(SettingsController.SETTINGS_PASSWORD_URL))
.andExpect(status().isOk())
.andExpect(model().attributeExists("account"))
.andExpect(model().attributeExists("passwordForm"));
}
@WithAccount("loosie")
@DisplayName("패스워드 수정 - 입력값 정상")
@Test
void updatePassword_success() throws Exception {
mockMvc.perform(post(SettingsController.SETTINGS_PASSWORD_URL)
.param("newPassword", "12345678")
.param("newPasswordConfirm", "12345678")
.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl(SettingsController.SETTINGS_PASSWORD_URL))
.andExpect(flash().attributeExists("message"));
Account loosie = accountRepository.findByNickname("loosie");
assertTrue(passwordEncoder.matches("12345678", loosie.getPassword()));
}
@WithAccount("loosie")
@DisplayName("패스워드 수정 - 입력값 에러 - 패스워드 불일치")
@Test
void updatePassword_fail() throws Exception {
mockMvc.perform(post(SettingsController.SETTINGS_PASSWORD_URL)
.param("newPassword", "12345678")
.param("newPasswordConfirm", "11111111")
.with(csrf()))
.andExpect(status().isOk())
.andExpect(view().name(SettingsController.SETTINGS_PASSWORD_VIEW_NAME))
.andExpect(model().hasErrors())
.andExpect(model().attributeExists("passwordForm"))
.andExpect(model().attributeExists("account"));
}
참고
'Dot Programming > Spring Clone' 카테고리의 다른 글
[스프링 웹앱 프로젝트 #30] ModelMapper적용 (0) | 2021.03.08 |
---|---|
[스프링 웹앱 프로젝트 #29] 알림 설정 (0) | 2021.03.05 |
[스프링 웹앱 프로젝트 #26] 프로필 이미지 변경 (0) | 2021.03.04 |
[스프링 웹앱 프로젝트 #25] 프로필 수정 테스트 (0) | 2020.12.22 |
[스프링 웹앱 프로젝트 #24] 프로필 수정 처리 (0) | 2020.12.21 |