3일차에 발생했던 오류를 해결하기위해 찾아보다가 thymeleaf의 layout 이라는 방법을 발견했고
적용시켜 보려했지만 잘 되지가 않아서 다른 방법을 생각해보다 오류를 자세히 보니 404 오류였고 localhost:8080/header.html 을 찾지 못한다고 씌여있었고 파일이 아니라 주소로 생각을 하는거 같아서 .html을 지우고 /header의 주소를 받아오는 GetMapping 함수를 만들어주니 해결이되었다.
@GetMapping("/header")
public String header() {
return "header";
}
@GetMapping("/footer")
public String footer() {
return "footer";
}
이렇게 만들어 준것이다. 효율적인지 아닌지는 조금더 공부를 해봐야할 것같다.
다음에 프로젝트를 진행할때는 Thymeleaf Layout을 사용해보자
Thymeleaf Layout
회원가입이나 로그인을 실패하거나 성공했을때에 알림을 주는 기능을 넣으려고한다.
자바스크립트에서 alert() 함수를 사용하면 가능하겠지만 우리가 로그인할때 사용하는 loadUserByUserName() 이라는 오버라이딩된 함수를 사용하는 중에 실패했다면 알림을 띄우고 싶었다.
찾아보다가 spring security로 로그인할때에 성공할때와 실패할때에 핸들러를 설정해줄수가 있었다.
http
.csrf().disable() //csrf 토큰 비활성화 (테스트시 걸어두는 게 좋음)
.authorizeRequests() //인증요청이들어올때
.antMatchers("/", "/auth/**", "/js/**", "/css/**", "/image/**", "/header", "/footer", "/test") //auth/밑으로 들어오면 + 폴더 허용도 해줘야한다
.permitAll() //누구나 허용
.anyRequest() //그밖에는
.authenticated() //인증이 되어야된다
.and()
.formLogin()
.loginPage("/auth/loginForm") //auth/ 밑이 아닌 주소들은 인증이필요하기때문에 /aut/loginForm으로 전달된다
.loginProcessingUrl("/auth/loginProc") //시프링 시큐리티가 해당 주소요청오는 로그인을 가로채고 대신 로그인해준다
.defaultSuccessUrl("/") //정상적일때 이 주소로 보낸다.
.failureHandler(customAuthenticationFailureHandler); //로그인 실패시 핸들러
return http.build();
그리고 따로 AuthenticationFailureHandler를 상속받는 custom 클래스를 만들어준다.
package com.example.board_project.handler;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
@Service
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// String errorMsg;
// if (exception instanceof BadCredentialsException) {
// errorMsg = "아이디 또는 비밀번호가 틀립니다!";
// }else if (exception instanceof InternalAuthenticationServiceException) {
// errorMsg = "내부적으로 발생한 시스템 문제로 인해 요청을 처리할 수 없습니다. 관리자에게 문의하세요.";
// } else if (exception instanceof UsernameNotFoundException) {
// errorMsg = "계정이 존재하지 않습니다. 회원가입 진행 후 로그인 해주세요.";
// } else if (exception instanceof AuthenticationCredentialsNotFoundException) {
// errorMsg = "인증 요청이 거부되었습니다. 관리자에게 문의하세요.";
// } else {
// errorMsg = "알 수 없는 이유로 로그인에 실패하였습니다 관리자에게 문의하세요.";
// }
// errorMsg = URLEncoder.encode(errorMsg, "UTF-8");
// response.sendRedirect("auth/loginForm?msg="+errorMsg);
response.sendRedirect("auth/loginForm?login=false");
}
}
지금은 경고창만 띄우고 싶기에 주석처리했다.
loginForm.html을 수정
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>로그인</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div id="header"></div>
<div class="container">
</br>
</br>
<h2>LOGIN</h2>
</br>
</br>
<form action="/auth/loginProc" method="post">
<div class="form-group">
<label for="loginId"> 아이디 : </label>
<input type="text" class="form-control" id="loginId" placeholder="Enter ID" name="username">
</div>
<div class="form-group">
<label for="password"> 비밀번호 : </label>
<input type="password" class="form-control" id="password" placeholder="Enter password" name="password">
</div>
<div th:if="${failLogin}">
<p id="valid" class="alert alert-danger">로그인에 실패하였습니다. 다시 시도해주세요!</p>
</div>
</br></br>
<button type="submit" class="btn btn-primary" >로그인</button>
</form>
</div>
<div id="footer"></div>
<div th:if="${failLogin}">
<script type="text/javascript">
alert("로그인 실패");
</script>
</div>
<script
src="https://code.jquery.com/jquery-3.7.0.js"
integrity="sha256-JlqSTELeR4TLqP0OG9dxM7yDPqX1ox/HfgiSLBj8+kM="
crossorigin="anonymous">
</script>
<script>
$(function() {
$("#header").load("/header");
$("#footer").load("/footer");
});
</script>
</body>
</html>
경고창을 띄우려고했지만 비동기통신이 아니기때문에 화면이 이동하면서 경고창이 발생했고 화면이 이동하면서 경고창이뜨는것은 자연스럽지가 않아서 로그인 입력칸 밑에 문구를 추가하기로함(타임리프사용)
이전에 하다가 포기했던 타임리프 layout 다음프로젝트때 한번 적용해보려했지만 지금 프로젝트에 다시 적용시키로 했다.
각각에 공통으로 들어가야할 html을 저장
저는 head, header, footer로 나눴습니다. 이 문서들은 templates/fragments/에 저장
head.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="headFragment">
HEAD
</div>
</html>
header.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="headerFragment">
HEADER
</div>
</html>
footer.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="footerFragment">
FOOTER
</div>
</html>
각각은 타임리프의 fragment 명을 정해준다 ex)footerFragment
정해진 fragment 들을 갖고 layout을 만들어준다
이때 xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 을 사용한다
/templates/layout/ 하에 저장
default_layout.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<th:block th:replace="fragments/head :: headFragment"></th:block>
</head>
<body>
<th:block th:replace="fragments/header :: headerFragment"></th:block>
<th:block layout:fragment="content"></th:block> //이부분에 내용이 들어간다
<th:block th:replace="fragments/footer :: footerFragment"></th:block>
</body>
</html>
들어갈 곳에 th:block태그를 사용해서 작성한다.
그 다음 내가 만들어놓은 내용이 들어갈 문서 작성
이번엔 layout:decorate="~{layout/default_layout}" 추가
test.html
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/default_layout}">
<div layout:fragment="content">
<div>CONTENT</div>
</div>
</html>
하면 div 태그의 코드가 default_layout의 fragment 명이 content인 부분에 들어가게된다
내 코드에 적용 성공
로그인 하지 않았을때에는 로그인과 회원가입 버튼만 보이게 설정하고 로그인을 완료했을때에는 글작성 회원 수정 등등의 버튼이 보이게 만들어보자
타임리프로 security 로그인한 정보를 가져오기 위해서는 gradle 의존성 추가
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
html에 네임스페이스 추가
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
@AuthenticationPrincipal PrincipalDetail principalDetail 사용해서 로그인한 사용자 정보 가져오고 model로 html 로 넘겨주고 타임리프로 변수 사용
controller
@GetMapping("/user/detail")
public String detail(Model model, @AuthenticationPrincipal PrincipalDetail principalDetail) {
User user = principalDetail.getUser();
System.out.println("-------------------------------"+user);
model.addAttribute("loginUser", user);
return "/user/detail";
}
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/default_layout}">
<div layout:fragment="content">
<div class="container">
</br>
</br>
<h2>내 정보</h2>
</br>
</br>
<div class="form-group">
<label for="loginId"> 아이디 : </label>
<input th:value="${loginUser.loginId}" type="text" class="form-control" id="loginId" placeholder="Enter ID" name="loginId" readonly>
</div>
<div class="form-group">
<label for="password"> 비밀번호 : </label>
<input th:value="${loginUser.password}" type="text" class="form-control" id="password" placeholder="Enter password" name="password" readonly>
</div>
<div class="form-group">
<label for="name"> 이름 : </label>
<input th:value="${loginUser.name}" type="text" class="form-control" id="name" placeholder="Enter Name" name="name" readonly>
</div>
<div class="form-group">
<label for="phoneNumber"> 휴대폰 번호 : </label>
<input th:value="${loginUser.phoneNumber}" type="text" class="form-control" id="phoneNumber" placeholder="Enter phonenumber" name="phoneNumber" readonly>
</div>
<div class="form-group">
<label for="username"> 닉네임 : </label>
<input th:value="${loginUser.username}" type="text" class="form-control" id="username" placeholder="Enter username" name="username" readonly>
</div>
</br></br>
</div>
</div>