[Zero-base] 6-14. 비밀번호 초기화 요청 및 메일 링크를 통한 초기화
중복 코드 간소화 - 네비게이션 바
- 공통부분에 해당하는 네비게이션 바의 html로 페이지를 만듦
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title> fastlms </title>
</head>
<body>
<h1>메인 페이지</h1>
<div th:fragment="fragment-body-menu">
<div>
<a href="/member/register">회원가입</a>
<a href="/member/info">회원정보</a>
<a href="/member/login">로그인</a>
<a href="/member/logout">로그아웃</a>
</div>
<hr/>
</div>
</body>
</html>
- 각 페이지 마다 fragment를 사용 할 수 있는 코드 추가
<div th:replace="/fragments/layout.html :: fragment-body-menu"></div>
- localhost:8080
- localhost:8080/member/register
- localhost:8080/member/info
로그인 페이지에서 비밀번호 찾기 구현
- 비밀번호 찾기 링크 추가
- 링크가 추가된 모습
- 페이지 맵핑
비밀번호 찾기 페이지 작성
- 이메일과 이름을 입력받고
- 비밀번호 초기화 요청 버튼 생성
- 로그인 없이 접근 가능한 페이지 추가
로그인 페이지에서 비밀번호 찾기 페이지 접근 테스트
- localhost:8080/member/login
- 비밀번호 찾기 링크 클릭
- localhost:8080/member/find/password
- 페이지 접근 가능
비밀번호 찾기 기능 구현
- 비밀번호 찾기를 눌렀을 때의 맵핑
- 입력받은 파라미터를 새로 지정
- ResetPasswordInput 파라미터를 받고
- sendResetPassword의 파라미터로 사용
- 결과를 불린값으로 저장
- 결과에 따라 비밀번호 찾기 결과 페이지로 보냄
- 비밀번호 찾기 요청 결과 페이지 생성
비밀번호 찾기 결과 페이지 테스트
- localhost:8080/member/find/password
- 비밀번호 초기화 요청
- 비밀번호 찾기 요청 결과 페이지 노출
비밀번호 찾기 기능 구현
public interface MemberService extends UserDetailsService {
boolean register(MemberInput parameter);
/**
* uuid에 해당하는 계정을 활성화 함.
*/
boolean emailAuth(String uuid);
/**
* 입력한 이메일로 비밀번호 초기화 정보를 전송
*/
boolean sendResetPassword(ResetPasswordInput parameter);
}
public interface MemberRepository extends JpaRepository<Member, String> {
Optional<Member> findByEmailAuthKey(String emailAuthKey);
Optional<Member> findByUserIdAndUserName(String userId, String userName);
}
- resetPasswordKey: 비밀번호 초기화 인증키
- restePasswordLimitDt: 비밀번호 초기화 유효시간
public class Member {
@Id
private String userId;
private String userName;
private String phone;
private String password;
private LocalDateTime regDt;
private boolean emailAuthYn;
private LocalDateTime emailAuthDt;
private String emailAuthKey;
private String resetPasswordKey;
private LocalDateTime resetPasswordLimitDt;
}
@Override
public boolean sendResetPassword(ResetPasswordInput parameter) {
Optional<Member> optionalMember = memberRepository.
findByUserIdAndUserName(
parameter.getUserId(),
parameter.getUserName());
if (!optionalMember.isPresent()) {
throw new UsernameNotFoundException("회원 정보가 존재하지 않습니다.");
}
Member member = optionalMember.get();
String uuid = UUID.randomUUID().toString();
member.setResetPasswordKey(uuid);
member.setResetPasswordLimitDt(LocalDateTime.now().plusDays(1));
memberRepository.save(member);
String email = parameter.getUserId();
String subject = "[fastlms] 비밀번호 초기화 메일입니다.";
String text = "<p>fastlms 비밀번호 초기화 메일입니다.</p>" +
"<p>아래 링크를 클릭하셔서 비밀번호를 초기화 해주세요.</p>" +
"<div><a target='_blank'" +
"href='http://localhost:8080/member/reset/password?id=" +
uuid + "'> 비밀번호 초기화 링크 </a></div>";
mailComponents.sendMail(email, subject, text);
return false;
}
비밀번호 찾기 기능 테스트
- 비밀번호 초기화 요청
- 비밀번호 찾기 요청 결과 페이지 노출
- 해당 값이 없는 케이스 대비
- 값이 없거나 틀린 경우 다시 시도 링크를 보여줌
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title> 회원 비밀번호 찾기 요청 결과 </title>
</head>
<body>
<h1>회원 비밀번호 찾기 요청 결과</h1>
<div th:replace="/fragments/layout.html :: fragment-body-menu"></div>
<div th:if="${result eq true}">
<p> 입력하신 이메일로 비밀번호 초기화 정보를 발송했습니다. </p>
</div>
<div th:if="${result eq false}">
<p> 요청하신 정보가 존재하지 않습니다. </p>
<div>
<a href="/member/find/password">다시 시도</a>
</div>
</div>
</body>
</html>
- 임의의 값 입력
- 해당 값이 없으므로 다시 시도 링크 노출
- 이메일 입력받는 부분의 형식을 'email'로 수정
- 비밀번호 초기화 요청 전 두 값 모두 <null>
- 이메일 및 이름 입력
- 비밀번호 초기화 요청
- 초기화를 위한 인증키 값과 유효기간 값 저장
- return 값 수정
- 회원 비밀번호 찾기 요청 결과
- 이메일로 비밀번호 초기화 정보 발송
- 인증하지 않고 다시 초기화 요청한 경우 인증키가 갱신됨
- 수신한 메일에서 비밀번호 초기화 링크 클릭
- 로그인 없이 접근 가능한 페이지 등록
- 페이지는 없으나 접속가능하고,
- 인증키 값이 보이는 것을 확인
비밀번호 초기화 페이지 구현
- 페이지 맵핑
- 회원 비밀번호 페이지 생성
비밀번호 초기화 페이지 연결 테스트
- 확인 비밀번호는 서버로 전송되는 값이 아니라
- 입력값이 맞는지 확인하는 용도로 클라이언트에서 확인용으로 사용
jQuery
- 다운받기(minified)
- 스크립트 복사
- 스크립트 붙여넣기
- alert() 테스트
- 입력 비빌번호를 출력
- 비밀번호 입력
- 비밀번호 재설정
- alert()를 이용하여 입력값이 다름을 확인
- 응용하여서 값이 받아온 뒤 비교 후
- 다른 경우 불일치 메시지 노출
- 불일치 테스트
- 불일치 메시지 노출

비밀번호 초기화 기능 구현
- resetPassword() 에서는 초기화 인증키 값(uuid)를 받아오고
- resetPasswordSumit()을 생성하고 맵핑
- password 변수 추가
- 비밀번호 초기화 페이지에서 uuid 값 노출
- 웹페이지에서 uuid 값이 보이는 것을 확인
- 변경할 비밀번호의 id를 저장할 입력값 지정
입력받은 비밀번호 parameter 값을 받아옴
- 입력받은 비밀번호 parameter값을 출력하는 부분 추가
- 회원 비밀번호 초기화 - 비밀번호 재설정
- 입력값이 정상적으로 넘어오는 것을 확인
- 비밀번호를 초기화할 ID와 비밀번호를 받아오고
- 결과를 boolean 값으로 출력
- 비밀번호를 초기화는 메소드 생성
- Implement methods 추가
- 정보가 없는 경우 출력값 지정
- Optional<Member> 추가
- resetPassword() 구현
@Override
public boolean resetPassword(String uuid, String password) {
Optional<Member> optionalMember = memberRepository.findByResetPasswordKey(uuid);
if (!optionalMember.isPresent()) {
throw new UsernameNotFoundException("회원 정보가 존재하지 않습니다.");
}
Member member = optionalMember.get();
//초기화 기간이 유효한지 체크
if (member.getResetPasswordLimitDt() == null) {
throw new RuntimeException(" 유효한 날짜가 아닙니다. ");
}
if (member.getResetPasswordLimitDt().isBefore(LocalDateTime.now())) {
throw new RuntimeException(" 유효한 날짜가 아닙니다. ");
}
String encPassword = BCrypt.hashpw(password, BCrypt.gensalt());
member.setPassword(encPassword);
member.setResetPasswordKey("");
member.setResetPasswordLimitDt(null);
memberRepository.save(member);
return true;
}
@PostMapping("/member/reset/password")
public String resetPasswordSubmit(
Model model,
ResetPasswordInput parameter) {
boolean result = false;
try {
result = memberService.resetPassword(
parameter.getId(), parameter.getPassword());
} catch (Exception e) {
}
model.addAttribute("result", result);
return "member/reset_password_result";
}
<body>
<h1>회원 비밀번호 초기화 결과</h1>
<!--<div th:replace="/fragments/layout.html :: fragment-body-menu"></div>-->
<div th:if="${result eq true}">
<p> 비밀번호가 초기화 되었습니다. </p>
<div>
<a href="/member/login">로그인 하기</a>
</div></div>
<div th:if="${result eq false}">
<p> 비밀번호가 초기화에 실패하였습니다. </p>
<div>
<a href="/member/find/password">로그인 다시 찾기</a>
</div></div>
</body>
비밀번호 초기화 기능 테스트
- 비밀번호 입력 - 재설정
- 비밀번호 초기화 결과 - 성공 - 로그인 하기
- 회원 로그인 페이지로 이동
uuid가 유효한지 확인하는 기능 구현
@Override
public boolean checkResetPassword(String uuid) {
Optional<Member> optionalMember = memberRepository.findByResetPasswordKey(uuid);
if (!optionalMember.isPresent()) {
return false;
}
Member member = optionalMember.get();
//초기화 기간이 유효한지 체크
if (member.getResetPasswordLimitDt() == null) {
throw new RuntimeException(" 유효한 날짜가 아닙니다. ");
}
if (member.getResetPasswordLimitDt().isBefore(LocalDateTime.now())) {
throw new RuntimeException(" 유효한 날짜가 아닙니다. ");
}
return true;
}
@GetMapping("/member/reset/password")
public String resetPassword(
Model model,
HttpServletRequest request) {
String uuid = request.getParameter("id");
boolean result = memberService.checkResetPassword(uuid);
model.addAttribute("result", result);
return "member/reset_password";
}
<body>
<h1>회원 비밀번호 초기화</h1>
<!--<div th:replace="/fragments/layout.html :: fragment-body-menu"></div>-->
<div th:if="${result eq true}">
<form method="post">
<div th:text="${uuid}"></div>
<div th:text="${parameter}"></div>
<div>
<input type="password"
name="password"
placeholder="비밀번호 입력" required/>
</div>
<div>
<input type="password"
name="rePassword"
placeholder="확인 비밀번호 입력" required/>
</div>
<div>
<button typre="submit"> 비밀번호 재설정 </button>
</div>
</form>
</div>
<div th:if="${result eq false}">
<p> 입력 값이 유효하지 않습니다. </p>
<div>
<a href="/"> 메인으로 이동 </a>
</div>
</div>
</body>
- 유효하지않은 인증 키값으로의 접근
- 초기화 진행되지 않음