64일차 내용 정리

채공부·2025년 8월 22일
  • 로그인된 사용자의 정보 출력
<strong sec:authentication="name"></strong>
  • HttpSession 객체에 로그인된 사용자의 userName 을 담아 놓기
session.setAttribute("userName", userName);
  • Spring Security 는 해당 객체와 사용자가 로그인 폼에 입력한 userName 과 password 를 비교해서 비밀번호가 일치하다면 로그인 처리를 대신 해준다
UserDetails ud = new User(dto.getUserName(), dto.getPassword(), authList);

Include

  • templates 폴더 안에 include 폴더 생성 후 resource.html 생성
  • th:block : 묶을 수 있는 Thymeleaf 블럭
  • 로딩할 자원 정보를 타입리프 블럭에 작성해 놓고 불러 쓸 수 있도록 한다
<th:block th:fragment="bootstrap">
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js" integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q" crossorigin="anonymous"></script>	
</th:block>
  • home.html 에 include 관련 코드 추가
<th:block th:insert="/include/resource :: bootstrap"></th:block>
  • home.html 에 include navbar 코드 추가
  • index 페이지에서 navbar 를 include 했다는 의미에서 nav() 에 index 전달
<th:block th:insert="/include/navbar :: nav('index')"></th:block>
  • navbar.html 생성
<nav th:fragment="nav(thisPage)" class="navbar navbar-expand-md bg-primary-subtle">
	<div class="container">
		<a class="navbar-brand" th:href="@{/}">Acorn</a>
		<button class="navbar-toggler" type="button"
			data-bs-toggle="collapse" data-bs-target="#navbarNav">
			<span class="navbar-toggler-icon"></span>
		</button>
		<div class="collapse navbar-collapse" id="navbarNav">
			<ul class="navbar-nav me-auto">
				<li class="nav-item">
					<a class="nav-link"
						th:classappend="${thisPage == 'member' ? 'active' : null}"
						th:href="@{/member/list}">Member</a>
				</li>
				<li class="nav-item">
					<a class="nav-link"
						th:classappend="${thisPage == 'book' ? 'active' : null}"
						th:href="@{/book/list}">Book</a>
				</li>
				<li class="nav-item">
					<a class="nav-link"
						th:classappend="${thisPage == 'board' ? 'active' : null}"
						th:href="@{/board/list}">Board</a>
				</li>
				<li class="nav-item">
					<a class="nav-link"
						th:classappend="${thisPage == 'gallery' ? 'active' : null}"
						th:href="@{/gallery/list}">Gallery</a>
				</li>
			</ul>
      	</div>
	</div>
</nav>
  • navbar.html 사용자 메뉴 코드 추가
<ul class="navbar-nav">

</ul>
    • 로그인 했을 때 출력할 내용
<th:block sec:authorize="isAuthenticated">
	<li class="nav-item me-2">
		<a class="nav-link p-0" th:href="@{/user/info}">
		<strong sec:authentication="name"></strong>
		</a>
	</li>
	<li class="nav-item me-2">
		<span class="navbar-text">Signed in</span>
	</li>
	<li class="nav-item">
		<a class="btn btn-danger btn-sm"
           th:href="@{/user/logout}">로그아웃</a>
	</li>
</th:block>
    • 로그인을 하지 않았을 때 출력할 내용
<th:block sec:authorize="!isAuthenticated">
	<li class="nav-item">
		<a class="btn btn-outline-light btn-sm me-2" 
           th:href="@{/user/loginform}">로그인</a>
	</li>
	<li class="nav-item">
		<a class="btn btn-warning btn-sm" 
           th:href="@{/user/signup-form}">회원가입</a>
	</li>
</th:block>

사용자 정보 자세히 보기

  • UserController 에 userInfo 메소드 생성
@GetMapping("/user/info")
public String userInfo() {
	// 로그인된 userName
	String userName = SecurityContextHolder.getContext().getAuthentication().getName();
	// 서비스 객체를 이용해서 사용자 정보를 얻어와서
		
	// Model 객체에 담은 다음
		
	// Thyemleaf 템플릿 페이지에서 응답		
	return "user/info";
}
  • UserService 인터페이스에 getUserByName 메소드 추가
public UserDto getUser(String userName);
  • UserServiceImpl 에 오버라이드
@Override
public UserDto getUser(String userName) {
	return dao.getByUserName(userName);
}
  • UserController 에 userInfo 메소드 코드 완성
@GetMapping("/user/info")
public String userInfo(Model model) {
	// 로그인된 userName
	String userName = SecurityContextHolder.getContext().getAuthentication().getName();
	// 서비스 객체를 이용해서 사용자 정보를 얻어와서
	UserDto dto = service.getUser(userName);
	// Model 객체에 담은 다음
	model.addAttribute("dto", dto);
	// Thyemleaf 템플릿 페이지에서 응답
	return "user/info";
}
  • info.html 생성
<div class="container">
	<h1>회원 가입 정보</h1>
	<table class="table table-bordered table-striped">
		<tr>
			<th>아이디</th>
			<td th:text="${dto.userName}"></td>
		</tr>
		<tr>
			<th>비밀번호</th>
			<td>
				<a th:href="@{/user/edit-password}">수정하기</a>
			</td>
		</tr>
		<tr>
			<th>이메일</th>
			<td th:text="${dto.email}"></td>
		</tr>
		<tr>
			<th>프로필 이미지</th>
			<td>
				<i th:if="${dto.profileImage == null}" style="font-size:100px;" class="bi bi-person-circle"></i>	
				<img th:unless="${dto.profileImage == null}"
					 th:src="@{/upload/{name}(name=${dto.profileImage})}"
					 style="width:100px;height:100px;border-radius:50%;"/>
			</td>
		</tr>
		<tr>
			<th>최종 수정 날짜</th>
			<td th:text="${dto.updatedAt}"></td>
		</tr>
		<tr>
			<th>가입 날짜</th>
			<td th:text="${dto.createdAt}"></td>
		</tr>
	</table>
	<a th:href="@{/user/edit}">개인 정보 수정(이메일, 프로필 사진)</a>
</div>
  • DB에 저장된 프로필 이미지의 saveFileName
  • UserDto 의 profileImage 라는 필드에 들어 있는 상태
@{/upload/aaa.jpg}
@{/upload/bbb.jpg}
@{/upload/ccc.jpg}

@{/upload/{경로변수명}(변수명=넣을값)}

@{/upload/{name}(name=${dto.profileImage})}

or

@{'/upload/' + ${dto.profileImage}}
  • resources 폴더 안에 custom.properties 생성
# file save location
file.location=C:/playground/upload
  • Spring08ExampleApplication 에 어노테이션 추가
@PropertySource(value="classpath:custom.properties")
  • ImageController 생성
@Controller
public class ImageController {
	
	@Value("${file.location}")
	private String fileLocation;
	
	@GetMapping("/upload/{saveFileName}")
	public ResponseEntity<InputStreamResource> image(@PathVariable("saveFileName") String name) throws IOException{ 
		String filePath = fileLocation + File.separator + name;
		File file = new File(filePath);
		if(!file.exists()) {
			throw new RuntimeException("file not found!");
		}
		String mimeType = Files.probeContentType(file.toPath());
		InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
		
		return ResponseEntity.ok()
				.contentType(MediaType.parseMediaType(mimeType))
				.contentLength(file.length())
				.body(isr);
	}
}

비밀번호 수정

  • edit-password.html 생성
<div class="container">
	<h1>비밀번호 수정 양식</h1>
	<form th:action="@{/user/update-password}" method="post" id="editForm">
		<div class="mb-2">
			<label class="form-label" for="password">기존 비밀번호</label>
			<input class="form-control" type="text" name="password" id="password" />
		</div>
		<div class="mb-2">
			<label class="form-label" for="newPassword">새 비밀번호</label>
			<input class="form-control" type="text" name="newPassword" id="newPassword" />
		</div>
		<div class="mb-2">
			<label class="form-label" for="newPassword2">새 비밀번호 확인</label>
			<input class="form-control" type="text" id="newPassword2" />
		</div>
		<button class="btn btn-sm bg-primary-subtle text-white" type="submit">수정하기</button>
	</form>
</div>
<script>
	const editor = new toastui.Editor({
		el:document.querySelector('#editor'),
		height:'500px',
		initialEditType:'wysiwyg',
		previewStyle:'vertical',
		language:'ko',
		initialValue:`<%=dto.getContent()%>`
	});
	document.querySelector("#editForm").addEventListener("submit", (e)=>{
		const content = editor.getHTML();
		console.log(content);
  		document.querySelector("#hiddenContent").value=content;
	});	
</script>

Exception

spring06.exception 패키지 생성

  • PasswordException 클래스 생성

  • 비밀 번호 수정시 입력한 비밀 번호가 DB 에 저장된 비밀번호와 일치하지 않을 때 발생시키는 Exception type

public class PasswordException extends RuntimeException{
	// 생성자
	public PasswordException(String message) {
		super(message); // 생성자의 매개 변수에 전달된 예외 메세지를 부모 생성자에 전달하면
		// 이 객체의 .getMessage() 메소드를 호출하면 해당 메세지가 리턴한다
	}
}
  • ExceptionController 클래스 생성

  • 예외 컨트롤러 @ControllerAdvice 어노테이션을 붙여서 bean 으로 만든다

@ControllerAdvice
public class ExceptionController {
	public String password(PasswordException pe) {
		return "redirect:/user/edit-password";
	}
}
  • UserController 에 updatePassword 메소드 생성
  • 비밀번호 수정 반영 요청 처리
@PostMapping("/user/update-password")
public String updatePassword() {
	return "user/update-password";
}
  • PwdChangeRequest 클래스 생성
@Data
public class PwdChangeRequest {
	private String userName;
	private String password;
	private String newPassword;
}
  • UserController 에 updatePassword 안에 코드 추가
@PostMapping("/user/update-password")
public String updatePassword(PwdChangeRequest pcr) {
	// pcr 객체에는 기존 비밀번호와 새 비밀번호가 들어 있다
    
	return "user/update-password";
}
  • UserService 인터페이스에 updatePassword 메소드 생성
public void updatePassword(PwdChangeRequest pcr);
  • UserServiceImpl 에 오버라이드 추가
@Override
public void updatePassword(PwdChangeRequest pcr) {
	// 로그인된 userName
	String userName = SecurityContextHolder.getContext().getAuthentication().getName();
	// DB 에 저장된 암호화된 비밀번호를 읽어온다
	UserDto dto = dao.getByUserName(userName);
	String encodedPwd = dao.getByUserName(userName).getPassword();
	// 암호화된 비밀번호와 입력한 비밀번호를 비교해서 일치하는지 확인
	boolean isValid = BCrypt.checkpw(pcr.getPassword(), encodedPwd);
	// 만일 일치하지 않으면 에외 발생시키기
	if(!isValid) {
		throw new PasswordException("기존 비밀번호가 일치하지 않습니다");
	}
// 일치하면 새 비밀번호를 암호화해서 UserDto 객체에 담은 다음 DB 에 수정 반영
	dto.setPassword(encoder.encode(pcr.getNewPassword()));		dao.updatePassword(dto);
}
  • UserController 에 updatePassword 메소드 완성
// 비밀번호 수정 반영 요청 처리
@PostMapping("/user/update-password")
public String updatePassword(PwdChangeRequest pcr, HttpSession session,
		HttpServletRequest req, HttpServletResponse res) {
	// pcr 객체에는 기존 비밀번호와 새 비밀번호가 들어 있다
	service.updatePassword(pcr);
	// 세션을 초기화ㅎ서 로그아웃 처리를 한다
	session.invalidate();
	// Security Logout Handler 객체를 이용해서 강제 로그아웃
	new SecurityContextLogoutHandler()
		.logout(req, res, SecurityContextHolder.getContext().getAuthentication());
		
	return "user/update-password";
}
  • update-password.html 생성
<div class="container">
	<p class="alert alert-success">
		비밀번호 수정 완료
		로그아웃 되었습니다
		<a class="alert-link" th:href="@{/user/loginform}">다시 로그인</a>
	</p>
</div>
  • ExceptionController 에 코드 추가
  • ExceptionHandler 또는 다른 일반 Controller 에서 리다이렉트 이동하면서 일회성 데이터를 전달하고 싶을 때는 컨트롤러의 매개변수에 RedirectAttributes 를 선언
  • 해당 객체에 데이터를 담으면 리다일렉트 이동된 view page 에서 해당 데이터를 추출해서 사용 가능
  • exception 이라는 키값으로 PasswordException 객체를 담는다
  • 해당 정보는 리다일렉트 이동된 view page 에서 사용할 Model 객체에 자동으로 담긴다
@ExceptionHandler(PasswordException.class)
public String password(PasswordException pe, RedirectAttributes ra) {
	ra.addFlashAttribute("exception", pe);
		
	return "redirect:/user/edit-password";
}
⭐ UserController 의 `"/user/edit-password"`
   요청을 처리하는 메소드가 호출되는 경우 2가지
   
   1. 비밀번호 수정 링크를 눌렀을 때
   
   2. 비밀번호 수정 정보를 입력하고 전송했지만 기존 비밀번호가 일치하지 않아서
      PasswordException 이 발생했을 때
  • edit-password.html 에 코드 추가

  • 만일 에러 객체가 존재한다면 아래의 알림 출력

<p class="alert alert-danger" th:if="${exception}" th:text="${exception.message}"></p>

JSON 응답

  • SecurityConfig 에 화이트 리스트 목록 추가
String[] whiteList = {"/", "/user/loginform", "/user/login-fail",
		"/user/expired", "/user/signup-form", "/user/signup", "/test/**"};
  • TestController 클래스 생성
  • @ResponseBody 어노테이션을 붙인 컨트롤러 메소드에서 Map 을 리턴하면
    { } 형식의 json 문자열이 응답된다
    { } 안에는 Map 에 담긴 key : value 데이터가 들어있다
@Controller
public class TestController {
	@GetMapping("/test/json01")
	@ResponseBody
	public Map<String, Object> json01(){
		Map<String, Object> map = Map.of(
			"num",1,
			"name","유재석",
			"isMan",true
		);
		return map;				
	}
    
    // json 문자열 응답 테스트를 위한 클래스 (dto 클래스)
	@Data
	public class MemberResponse{
		private int num;
		private String name;
		private boolean isMan;
	}
	
	@GetMapping("/test/json02")
	@ResponseBody
	public MemberResponse json02() {
		MemberResponse m = new MemberResponse();
		m.setNum(2);
		m.setName("박명수");
		m.setMan(false);
		
		return m;
	}
    
    @GetMapping("/test/json03")
	@ResponseBody
	public List<String> json03(){
		List<String> list = List.of("유재석", "박명수", "정준하");
		return list;
	}
    
    @GetMapping("/test/json04")
	@ResponseBody
	public List<MemberResponse> json04(){
		MemberResponse m1 =new MemberResponse();
		m1.setNum(1);
		m1.setName("유재석");
		m1.setMan(true);
		
		MemberResponse m2 =new MemberResponse();
		m2.setNum(2);
		m2.setName("박명수");
		m2.setMan(false);
		
		List<MemberResponse> list = List.of(m1, m2);
		
		return list;
	}
}
{
"isMan": true,
"num": 1,
"name": "유재석"
}

{
"num": 2,
"name": "박명수",
"man": false
}

[
"유재석",
"박명수",
"정준하"
]

{
"num": 1,
"name": "유재석",
"man": true
},
{
"num": 2,
"name": "박명수",
"man": false
}
  • Map or Dto{ } 형식의 json

  • List[ ] 형식의 json

  • List<String>["xxx", "yyy", "zzz"]
    List<Map>[{ }, { }, { }]
    List<Dto> [{ }, { }, { }]

회원가입

  • UserController 에 checkId 메소드 생성

  • 사용 가능한 아이디 인자 여부를 json 문자열로 리턴하는 메소드

@GetMapping("/user/check-id")
@ResponseBody
public Map<String, Object> checkId(String inputId){
	return null;
}
  • signup-form.html 생성
<div class="container">
	<h1>회원가입 양식</h1>
	<form th:action="@{/user/signup}" method="post" novalidate>
		<div class="mb-2">
			<label class="form-label" for="userName">아이디</label>
			<input class="form-control" type="text" name="userName" id="userName"/>
			<small class="form-text">영문자 소문자로 시작하고 5글자~10글자 이내로 입력하세요</small>
			<div class="valid-feedback">사용 가능한 아이디 입니다.</div>
			<div class="invalid-feedback">사용할 수 없는 아이디 입니다.</div>
		</div>
		<div class="mb-2">
			<label class="form-label" for="pwd">비밀번호</label>
			<input class="form-control" type="password" name="password" id="pwd"/>
			<small class="form-text">특수 문자를 하나 이상 조합하세요.</small>
			<div class="invalid-feedback">비밀 번호를 확인 하세요</div>
		</div>
		<div class="mb-2">
			<label class="form-label" for="pwd2">비밀번호 확인</label>
			<input class="form-control" type="password" id="pwd2"/>
		</div>
		<div class="mb-2">
			<label class="form-label" for="email">이메일</label>
			<input class="form-control" type="email" name="email" id="email"/>
			<div class="invalid-feedback">이메일 형식에 맞게 입력하세요.</div>
		</div>
		<button class="btn btn-primary btn-sm" type="submit" disabled>가입</button>
	</form>
</div>
<script>
	// 아이디 유효성 여부를 관리할 변수
	let isIdValid = false;
	// 비밀번호 유효성 여부를 관리할 변수
	let isPwdValid = false;
	// 이메일 유효성 여부를 관리할 변수
	let isEmailValid = false;
		
	const checkForm = () => {
	// 만일 아이디도 유효하고 그리고 비밀번호도 유효하고 그리고 이메일도 유효하다면
	if(isIdValid && isPwdValid && isEmailValid){
			// 전송 버튼에 disabled 속성을 제거하고
			document.querySelector("[type=submit]").removeAttribute("disabled");				
		}else{
			// 전송 버튼에 disabled 속성을 추가한다
			document.querySelector("[type=submit]").setAttribute("disabled", "");
		}			
	};
		
	// blur 는 포커스를 잃었을 때 발생하는 이벤트
	document.querySelector("#userName").addEventListener("blur", (e)=>{
		// 현재까지 입력한 아이디를 읽어온다.
		const inputId = e.target.value;
		// "영문자 소문자로 시작하고 5글자~10글자 이내로 입력" 이 검증조건을 만족했는지 여부를 알아낸다
		const reg = /^[a-z].{4,9}$/;
		// 만일 정규 표현식을 통과 하지 못했다면 
		if(!reg.test(inputId)){
			e.target.classList.remove("is-valid");
			e.target.classList.add("is-invalid");
			//사용할수 없는 아이디라는 의미에서 false 를 넣어준다.
			isIdValid=false;
			checkForm();
			return; //함수를 여기서 종료하기 
		}
		// fetch() 함수를 이용해서 get 방식으로 입력한 아이디를 보내고 사용가능 여부를 json 으로 응답받는다.
		fetch("[[@{/}]]user/check-id?inputId="+inputId)
		.then(res=>res.json())
		.then(data=>{
			// 일단 is-valid , is-invalid 클래스를 제거하고
			e.target.classList.remove("is-valid", "is-invalid");
			// data 는 {canUse:true} 또는 {canUse:false} 구조의 object 이다
			if(data.canUse){ //사용가능 하다면
				e.target.classList.add("is-valid");
				// 사용할수 있는 아이디라는 의미에서 true 를 넣어준다
				isIdValid = true;
			}else{//사용가능 하지 않다면 
				e.target.classList.add("is-invalid");
				//사용할수 없는 아이디라는 의미에서 false 를 넣어준다
				isIdValid = false;
			}
			checkForm();
		});
	});
	// 함수를 미리 만들어서 
	const checkPwd = ()=>{
			
		// 비밀 번호를 검증할 정규 표현식(특수문자 포함여부)
		const reg_pwd=/[\W]/;
			
		// 양쪽에 입력한 비밀번호를 읽어와서
		let pwd=document.querySelector("#pwd").value;
		let pwd2=document.querySelector("#pwd2").value;
			
		// 일단 is-valid 와 is-invalid 클래스를 제거를 먼저하고 
		document.querySelector("#pwd").classList.remove("is-valid", "is-invalid");
			
		// 일단 정규표현식을 만족하는지 확인해서 만족하지 않으면 함수를 여기서 종료
		// 만일 첫번째 비밀번호가 정규표현식을 통과하지 못하거나 또는 두번째 비밀번호가 정규표현식을 통과하지 못한다면
		if( !reg_pwd.test(pwd) || !reg_pwd.test(pwd2) ){
			document.querySelector("#pwd").classList.add("is-invalid");
			isPwdValid = false;
			checkForm();
			return;
		}
			
		if(pwd == pwd2){
				document.querySelector("#pwd").classList.add("is-valid");
			// 비밀번호가 유효하다는 의미에서 true 를 넣어준다
		isPwdValid = true;
				
		}else{
			document.querySelector("#pwd").classList.add("is-invalid");
			// 비밀번호가 유효하지 않다는 의미에서 false 를 넣어준다
			isPwdValid = false;
		}
		checkForm();
	};
		
	// 비밀번호 혹은 비밀번호 확인란에 입력 했을때 동일한 함수가 호출되도록 한다
	document.querySelector("#pwd").addEventListener("input", checkPwd);
	document.querySelector("#pwd2").addEventListener("input", checkPwd);
		
	document.querySelector("#email").addEventListener("input", (e)=>{
			
		//이메일을 검증할 정규 표현식
		const reg_email=/@/;
			
		//입력한 문자열 읽어오기  (e.target 은 이벤트가 발생한 바로 그 요소의 참조값이다)
		const email=e.target.value;
			
		//일단 is-valid 와 is-invalid 클래스를 제거를 먼저하고 
		document.querySelector("#email").classList.remove("is-valid", "is-invalid");
			
		if(reg_email.test(email)){
			document.querySelector("#email").classList.add("is-valid");
			isEmailValid = true;
		}else{
		document.querySelector("#email").classList.add("is-invalid");
			isEmailValid = false;
		}
		checkForm();
	});		
</script>
  • UserService 인터페이스에 canUseId 메소드 생성
public Map<String, Object> canUseId(String id);
  • UserServiceImpl 에 오버라이드 추가
@Override
public Map<String, Object> canUseId(String id) {
	// id 를 이용해서 DB 에 해당하는 아이디로 가입된 정보가 있는지 읽어와 본다 (없으면 null)
	UserDto dto = dao.getByUserName(id);
	// id 가 사용 가능한지 여부 (dto 가 null 이면 사용가능한 아이디)
	boolean canUse = dto == null ? true : false;
	// Map 에 담아서 리턴한다
	return Map.of("canUse", canUse);
}
  • UserService 에 checkId 메소드 완성
// 사용 가능한 아이디 인자 여부를 json 문자열로 리턴하는 메소드
@GetMapping("/user/check-id")
@ResponseBody
public Map<String, Object> checkId(String inputId){
	return service.canUseId(inputId);
}
  • SecurityConfig 에 화이트리스트 코드 추가
String[] whiteList = {"/", "/user/loginform", "/user/login-fail",
			"/user/expired", "/user/signup-form", "/user/signup",
			 "/test/**", "/user/check-id"};
profile
학원 공부 내용 정리

0개의 댓글