Ex) 메인 페이지에 공통적으로 계속 사용할 코드를 어떻게 관리할까?
- ThymeLeaf fragment
- layout 파일을 이렇게 구성하면
<hide/>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>fastlms</title>
</head>
<body>
<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>
- index 파일을 간단히 작성 가능
<hide/>
<!doctype html>
<html lang="ko" 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>
<hr/>
</body>
</html>
- 다른 info, 파일에도 모두 적용한다.
<div th:replace="/fragments/layout.html :: fragment-body-menu" ></div>
- 가입 결과 페이지는 true / false에 맞게 각각 넣어줘야한다.
Note) 실행 결과
==================================오류 =====================================
- 로그인을 해야만 메인페이지가 안 뜬다.
- 그리고 메인 기본 인덱스 페이지가 안 나오고 로그인 페이지가 첫 번째로 나온다.
- 원인: SecurityConfig클래스에서 메서드 이름을 잘못 썼다.
Ex) 비밀 번호 찾기
- 로그인 페이지
- member/find/password는 @PostMapping() 안의 매개변수 - URL 주소
<hide/>
<!DOCTYPE html>
<html lang="ko" 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:text="${errorMessage}">
</div>
<form method="post">
<div>
<input type="text" name="username" placeholder="아이디(이메일)을 입력해주세요"/>
</div>
<div>
<input type="password" name="password" placeholder="비밀번호 입력"/>
</div>
<div>
<button type="submit" >로그인</button>
</div>
<div>
<a href="/member/find/password">비밀번호 찾기</a>
</div>
</form>
</body>
</html>
- 멤버 컨트롤러
<hide/>
@GetMapping("/member/find/password")
public String findPassword(){
return "member/find_password";
}
- find_password 파일 만든다.
-> 데이터베이스의 변수명을 보고 만든다
<hide/>
<!DOCTYPE html>
<html lang="ko" 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>
<form method="post">
<div>
<input type="text" name="userId" placeholder="아이디(이메일)을 입력"/>
</div>
<div>
<input type="text" name="userName" placeholder="이름 입력"/>
</div>
<div>
<button type="submit" >비밀번호 초기화 요청</button>
</div>
</form>
</body>
</html>
- http://localhost:8080/member/find_password 입력하면 http://localhost:8080/member/login페이지로 이동한다.
-> SecurityConfig 클래스 설정 때문이다.
-> SecurityConfig 에 내용 추가
(find_password 수정
<hide/>
http.authorizeRequests()
.antMatchers(
"/"
, "/member/register"
, "/member/email-auth"
, "/member/find/password"
)
.permitAll();
Note) 실행 결과
- ResetPasswordInput
<hide/>
package com.zerobase.fastlms.member.model;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class ResetPasswordInput {
private String userId;
private String userName;
}
- 비밀번호 찾기 메서드
-> 새로운 비밀번호를 파라미터로 받는다.
-> 주소는 그대로 뷰만 바꾸고 메인 페이지로 넘기려면? => return "index" =>
-> 주소도 같이 바꾸고 메인 페이지로 넘기려면? => return "redirect:/"
-> 인터페이스 model에 속성을 추가한다.
<hide/>
@PostMapping("/member/find/password")
public String findPasswordSubmit(
Model model,
ResetPasswordInput parameter){
// boolean result = memberService.sendResetPassword(parameter); // 초기화 메일 보낸 결과
// model.addAttribute("result", result);
return "member/find_password_result";
}
- 맞는지 확인하기
- find_password_result.html : 비밀번호 찾기 결과 페이지
<hide/>
<!DOCTYPE html>
<html lang="ko" 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>-->
<p>입력하신 이메일로 비밀번호 초기화 정보를 보내드렸습니다.</p>
</body>
</html>
Note) 실행 결과
- result 만들어줘야한다. (이메일과 이름 맞는지)
- 이메일 보내준다.
- 멤버서비스
<hide/>
/**
* 입력한 이메일로 비밀번호 초기화 정보를 전송
*/
boolean sendResetPassword(ResetPasswordInput parameter);
- 멤버리포지토리 - 메서드 findByUserIdAndUserName() 추가
<hide/>
package com.zerobase.fastlms.member;
import com.zerobase.fastlms.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, String> {
Optional<Member> findByEmailAuthKey(String emailAuthKey);
Optional<Member> findByUserIdAndUserName(String userId, String userName);
}
- 멤버서비스임플 - sendResetPassword()
-> 사용자한테 메일을 보내고 나서 사용자가 링크를 타고 와서 아이디 패스워드를 입력 => 입력한 데이터로 계정 초기화
-> 사용자만 아는 유일한 값을 보낸다.
- Member
-> 키가 일치할 때만 비밀번호 초기화 가능하도록 한다.
private String resetPasswordKey;
-> 시간 초과하면 더이상 초기화할 수 없다.
private LocalDateTime resetPasswordLimitDt;
- 멤버서비스임플
-> id는 url에 나오는 특정한 값이다.
<hide/>
@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;
}
Note) 실행 결과
Ex) 회원 정보 입력해서 비밀번호 찾기
<hide/>
@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 true;
}
- ServiceImpl에 내용을 추가한다.
<hide/>
http.authorizeRequests()
.antMatchers(
"/"
, "/member/register"
, "/member/email-auth"
, "/member/find-password"
, "/member/reset/password"
)
.permitAll();
- 멤버컨트롤러
<hide/>
@GetMapping("/member/reset/password")
public String resetPassword(){
return "member/reset_password";
}
- 확인 비밀번호는 서버로 데이터를 보낼 필요 없이 클라이언트에서 맞는지 체크하면 된다. jQuery
Note) 비밀 번호 찾기 => 회원 정보 일치
- 데이터베이스에 데이터가 들어온다. 인증 날짜, 인증 키
- 이메일 링크 클릭 => 비번 확인은 나중에 구현한다. jQuery
Note) 비밀 번호 찾기 => 틀린 정보 입력
Ex) jQuery 다운로드
- 접속 https://releases.jquery.com/
- 압축되지 않은 minified 선택
- copy 버튼을 누른다.
- reset_password 파일에 추가
->
<hide/>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(document).ready(function (){
alert('1');
});
</script>
- 비밀번호 초기화 화면에서 새로고침 누르면?
-> 아래와 같은 알림창이 뜬다.
Ex) 비밀번호 재설정 버튼 누르고 난 다음, 페이지 만든다.
- reset_password 파일에서 버튼은 하나밖에 없다.
- password를 찾아서 값이 같은지 확인한다.
- this: 현재 폼, input의 이름이 password인 값을 찾아서 저장한다.
var password = $(this).find('input[name=password]').val();
- 비밀번호를 다르게 입력하고 비밀번호 재설정 버튼을 누르면?
<hide/>
<script>
$(document).ready(function (){
$('form').on('submit', function () {
const password = $(this).find('input[name=password]').val();
const rePassword = $(this).find('input[name=rePassword]').val();
if(password != rePassword){
alert("비밀번호가 일치하지 않습니다.");
return false;
}
});
// return true; 없어야 서버로 데이터 전송된다.
});
</script>
Note) 실행 화면
- 비밀번호 초기화 메서드
-> 비밀번호 재설정 창에서 넘어온 값들을 받아서 비밀번호를 새로 지정한다.
- ResetPasswordInput()
<hide/>
package com.zerobase.fastlms.member.model;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class ResetPasswordInput {
private String userId;
private String userName;
private String password;
}
- 비밀번호 재설정 화면
- 재설정 버튼 누른 화면
-> 화면에 나온 값을 가지고 아이디를 초기화할 계획이다.
- 멤버컨트롤러
<hide/>
@PostMapping("/member/reset/password")
public String resetPasswordSubmit(Model model, ResetPasswordInput parameter){
model.addAttribute("parameter", parameter);
boolean result = memberService.resetPassword(parameter.getUserId(), parameter.getPassword());
return "member/reset_password";
}
- 멤버서비스 메서드 추가한다.
<hide/>
/**
* 입력받은 uuid에 대해서 password로 초기화한다
* */
boolean resetPassword(String userId, String password);
- MemberServiceImpl에 메서드 추가
<hide/>
@Override
public boolean resetPassword(String uuid, String password) {
Optional<Member> optionalMember = memberRepository.findById(uuid);
if(!optionalMember.isPresent()){
throw new UsernameNotFoundException("회원 정보가 존재하지 않습니다.");
}
return false;
}
- 멤버리포짓에 추가
Optional<Member> findByResetPasswordKey(String resetPasswordKey);
- resetPassword() 구현
-> 패스워드 초기화 요청할 때, 날짜가 맞아야만 초기화 가능하다.
<hide/>
@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;
}
- 멤버 컨트롤러 - resetPasswordSubmit()
<hide/>
@PostMapping("/member/reset/password")
public String resetPasswordSubmit(Model model, ResetPasswordInput parameter){
model.addAttribute("parameter", parameter);
boolean result = false;
try{
result = memberService.resetPassword(parameter.getUserId(), parameter.getPassword());
}catch (Exception e){
e.printStackTrace();
}
model.addAttribute("result", result);
return "member/reset_password_result";
}
- 비밀번호 초기화한 다음, 결과 나타낼 파일 reset_password_result 을 만든다.
-> 비밀번호 초기화 결과는 위에 컨트롤러의 resetPasswordSummit의 결과 "result"를 보고 판단한다.
<hide/>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원 비밀번호 초기화 결과</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(document).ready(function (){
$('form').on('submit', function () {
const password = $(this).find('input[name=password]').val();
const rePassword = $(this).find('input[name=rePassword]').val();
if(password != rePassword){
alert("비밀번호가 일치하지 않습니다.");
return false;
}
});
// return true; 없어야 서버로 데이터 전송된다.
});
</script>
</head>
<body>
<h1>회원 비밀번호 초기화 결과</h1>
<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>
</html>
- 비밀번호 초기화 결과
- 초기화가 정상적으로 이뤄지면 데이터가 지워진다.
-> 그런데 문제는 여전히 이메일에서 링크를 타고 들어올 수 있다는 것이다.
-> 이 문제를 방지하려면?
-> 컨트롤러의 resetPassword() 에서 uuid가 유효한지 확인해야한다.
- 컨트롤러를 내용 추가
<hide/>
@GetMapping("/member/reset/password")
public String resetPassword(Model model, HttpServletRequest request){
String uuid = request.getParameter("id");
model.addAttribute("uuid", uuid);
boolean result = memberService.checkResetPassword(uuid);
return "member/reset_password";
}
- 멤버서비스
->uuid를 매개변수로 넣었을 때, 적잘한 값인지 체크하기 위한 메서드 "checkResetPassword()"
/**
* 입력받은 uuid 유효성 검사
*/
boolean checkResetPassword(String uuid);
- 멤버서비스 임플 위 메서드 구현
-> result를 그대로 컨트롤러의 model에 저장한다.
<hide/>
@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;
}
<hide/>
@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";
}
- 그러고나서 비밀번호 초기화 창에서 위 변수를 이용 가능하다.
- 위 데이터베이스에서 reset_password_limit_dt 값이 null 인 경우, 아래와 같은 창을 띄울 수 있다.
'Spring Projcect > 학습 관리 시스템 & 백오피스 구축' 카테고리의 다른 글
Chapter 10. 회원 목록 (0) | 2022.08.20 |
---|---|
Chapter 09. 관리자 로그인 구현 (0) | 2022.08.20 |
Chapter 07. 스프링 부트 프로젝트 - 로그인/로그아웃 (0) | 2022.08.18 |
Chapter 05. 스프링부트 기반 웹 프로젝트 구성 (0) | 2022.08.09 |
Chapter 04. MariaDB 설치 및 환경설정 (0) | 2022.08.09 |