반응형
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{common/layout}">
<section layout:fragment="contents">
<div class="container wrap__content">
<div class="col-md-6 col-md-offset-3 container">
<form class="form-horizontal">
<div class="form-group">
<label for="inputEmail3" class="col-sm-2 control-label">Email</label>
<div>
<input type="email" class="form-control" id="inputEmail3" placeholder="Email">
</div>
</div>
<div class="form-group">
<label for="inputPassword3" class="col-sm-2 control-label">password</label>
<div>
<input type="password" class="form-control" id="inputPassword3" placeholder="Password">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-5">
<button type="button" class="btn btn-primary" data-toggle="modal"
data-target="#accountUserModal">Sign up</button>
<button type="submit" class="btn btn-default">Sign in</button>
</div>
</div>
</form>
<div class="modal fade" id="accountuserModal" tabindex="1" role="dialog"
aria-labelledby="accountUserLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="accountUserLabel">Sign up</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="user_name">이름</label> <input type="text" class="form-control" id="userName"
placeholder="이름 입력">
</div>
<div class="form-group">
<label for="user_id">아이디</label> <input type="text" class="form-control" id="userId"
placeholder="아이디 입력">
</div>
<div class="form-group">
<label for="password">이름</label> <input type="password" class="form-control" id="password"
placeholder="패스워드 입력">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">취소</button>
<button type="button" class="btn btn-primary" id="btn-save">가입</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</html>
스프링 시큐리티는 스프링에서 만들어지는 애플리케이션에 보안 기능을 담당하는 프레임워크이다
Principal - 접근 주체. 보호된 리소스에 접근하는 대상
Authentication - 인증. 접근하는 대상이 누구인지 확인하는 과정
Authorize - 인가. 리소스에 접근 권한이 있는지 확인하는 과정
권한 : 리소스에 대한 접근 제한에 대해 어떤 권한을 가지고 있는지 확인
스프링 시큐리티는 필터 기반으로, 주요 로직들은 모두 필터를 거치고 난 뒤에 디스패처서블릿을 거치고 컨트롤러에 전달된다
https://velog.io/@allen/스프링-시큐리티와-인증
build.gradle에 스프링 시큐리티와 thymeleaf에 스프링 시큐리티 사용을 가능하게 해주는 디펜던시 추가
...
// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
// thymeleaf에서 스프링 시큐리티를 사용 가능하게 해줌
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
SecurityConfig.java
스프링 시큐리티의 설정 클래스
package com.cos.web01.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.cos.web01.service.UserSecurityService;
import lombok.AllArgsConstructor;
@Configuration // 이 클래스가 설정 파일임을 스프링에 알려줌
@EnableWebSecurity // 스프링 시큐리티 필터 체인이 자동으로 포함됨
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// WebSecurityConfigurerAdapter 클래스를 상속 받아 시큐리티 설정
@Autowired
private UserSecurityService userSecurityService;
@Bean
public PasswordEncoder passwordEncoder() {
// 스프링 시큐리티에서 제공하는 암호화 객체
// Bean으로 등록하여 Service에서 사용할 수 있도록 함
return new BCryptPasswordEncoder();
}
@Override
public void configure(WebSecurity web) throws Exception {
// WebSecurityConfigurerAdapter 클래스의 configure 메소드들을 오버라이딩하여 시큐리티를 설정
// static 폴더에 있는 파일들은 인증 과정 무시하기
web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.authorizeRequests().antMatchers("/board").hasRole("USER").antMatchers("/**").permitAll()
// /board에 대한 요청 처리 설정, 요청을 보내는 대상의 ROLE을 확인하여 USER이면 접근 허락
//
// .formLogin().loginPage("/").loginProcessingUrl("/user/login").defaultSuccessUrl("/board", true)
// .failureHandler(loginFailHandler()).permitAll().and() //
// formLogin() - form 기반으로 로그인 인증을 하도록 설정
// loginPage("/") - /login 경로로 접속하면 스프링 시큐리티에서 제공하는 자체 form을 사용하는데, 직접 만든 form을 사용하는 설정
// loginProcessingUrl("/user/login") - 매개 변수로 전달한 주소에 대해 요청을 보내면 로그인 진행, 직접 만든 form 태그의 action 주소를 매개 변수로 전달한 주소와 맞추어야함
// defaultSuccessUrl("/board", true) - 로그인이 정상적으로 이루어졌을 때, 어떤 페이지로 이동할 것인지 설정
// failureHandler(loginFailHandler()) - 로그인에 실패했을 때 어떻게 처리할 것인지 설정, 따로 작성한 loginFailHandler 메소드를 전달
//
// .logout().logoutRequestMatcher(new AntPathRequestMatcher("/user/logout")).logoutSuccessUrl("/")
// .invalidateHttpSession(true).and() //
// logout() - 로그아웃 설정 시작
// logoutRequestMatcher(new AntPathRequestMatcher("/user/logout")) - 로그아웃 설정, 전달한 경로에 대한 요청이 생기면 로그아웃 진행
// logoutSuccessUrl("/") - 로그아웃 성공시 이동할 페이지 주소 설정
// invalidateHttpSession(true) - 로그아웃 성공 시 Http 세션 초기화
//
// exceptionHandling().accessDeniedPage("/login/error")
// 페이지에 대한 접근 권한이 없을 때 처리 메소드, /login/error 페이지로 이동하게 함
http.authorizeRequests().antMatchers("/board").hasRole("USER").antMatchers("/**").permitAll().and() //
.formLogin().loginPage("/").loginProcessingUrl("/user/login").defaultSuccessUrl("/board", true)
.failureHandler(loginFailHandler()).permitAll().and() //
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/user/logout")).logoutSuccessUrl("/")
.invalidateHttpSession(true).and() //
.exceptionHandling().accessDeniedPage("/login/error");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 시큐리티에 대한 로그인 처리 설정
auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());
}
@Bean
public AuthenticationFailureHandler loginFailHandler() {
// 로그인 오류 처리 설정
System.out.println("SecurityConfig loginFailHandler() 호출");
return new LoginFailHandler();
}
}
UserRepository.java
아이디로 검색할 수 있도록 메소드 추가
package com.cos.web01.domain.user;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<Users, Long> {
// userId로 검색할 수 있는 메소드
// Optional을 사용하여 null을 사용하지 않고 Optional 인스턴스로 대체하여 값이 없음에 대한 에러를 안전하게 처리
Optional<Users> findByUserId(String userId);
}
LoginFailureHandler.java
로그인 실패 시 에러 메세지를 controller의 /login/fail로 전달
package com.cos.web01.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
public class LoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// AuthenticationFailureHandler 인터페이스를 구현하여 로그인 실패시에 처리할 핸들러 설정
//
// 로그인에 실패할 시 실패한 내용을 화면에 출력 작업
String errorMsg = exception.getMessage();
request.setAttribute("errorMsg", errorMsg);
System.out.println("로그인 에러");
request.getRequestDispatcher("/login/fail").forward(request, response);
}
}
WebRestController.java
package com.cos.web01.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.cos.web01.domain.dto.UserSaveRequestDto;
import com.cos.web01.domain.user.UserRepository;
import com.cos.web01.service.UserSecurityService;
import lombok.AllArgsConstructor;
@RestController
@AllArgsConstructor
public class WebRestController {
@Autowired
private UserSecurityService userSecurityService;
@PostMapping("/users/signup")
public ResponseEntity<Map<String, Object>> saveUsers(@RequestBody UserSaveRequestDto dto) {
userSecurityService.accountUser(dto);
Map<String, Object> map = new HashMap<>();
map.put("msg", "save");
return new ResponseEntity<>(map, HttpStatus.OK);
}
}
WebController.java
package com.cos.web01.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import lombok.AllArgsConstructor;
@Controller
@AllArgsConstructor
public class WebController {
@GetMapping({ "", "/" })
public String index() {
return "contents/index";
}
@GetMapping("/board")
public String hello() {
return "contents/board";
}
@GetMapping("/login/error")
public String error() {
return "contents/error";
}
@PostMapping("/login/fail")
public String initPost() {
return "contents/index";
}
@GetMapping("/info")
public String info() {
return "contents/info";
}
}
layout.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- https://hyunsangwon93.tistory.com/28 -->
<!-- 스프링 부트 csrf ajax 전송 방법 -->
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
<title>web01</title>
<!-- 부트스트랩 -->
<link rel="stylesheet" href="/css/lib/bootstrap.min.css" />
<style>
body {
margin: 0;
padding: 0;
}
.wrap__content {
margin-top: 100px;
}
header, .nav {
padding: 20px;
}
</style>
</head>
<body>
<header>
<h4>simple web</h4>
</header>
<ul class="nav nav-tabs">
<!-- 사용자가 USER 권한이 없을 때 -->
<li th:if="!${#request.isUserInRole('USER')}" role="presentation"><a href="/"><button
class="btn btn-sm btn-outline-secondary">MAIN</button></a></li>
<!-- th:href - thymeleaf에서 url 표현 -->
<!-- https://zamezzz.tistory.com/308 -->
<li sec:authorize="hasRole('ROLE_USER')" role="presentation"><a th:href="@{/board}"><button
class="btn btn-sm btn-outline-secondary">게시판</button></a></li>
<li sec:authorize="hasRole('ROLE_USER')" role="presentation"><a th:href="@{/info}"><button
class="btn btn-sm btn-outline-secondary">내 정보</button></a></li>
<li sec:authorize="hasRole('ROLE_USER')" role="presentation"><a th:href="@{/user/logout}"><button
class="btn btn-sm btn-outline-secondary">로그아웃</button></a></li>
</ul>
<section layout:fragment="content"></section>
<footer>
<div class="footer-copyright text-center py-3">footer</div>
</footer>
<!-- Jquery, bootstrap -->
<script src="/js/lib/jquery.min.js"></script>
<script src="/js/lib/bootstrap.min.js"></script>
</body>
</html>
board.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{common/layout}">
<section layout:fragment="content">
<h3>게시판</h3>
<div class="container">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>#</th>
<th>Col2</th>
<th>Col3</th>
<th>Col4</th>
<th>Col45</th>
</tr>
</thead>
<tbody>
<!-- 게시글 목록 표시 필요 -->
</tbody>
</table>
</div>
<div align="right" class="col-md-12 left">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#savePostsModal">
글 등록</button>
</div>
<div class="center-block" style="width: 300px; padding: 15px;">
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item"><a class="page-link" href="#">Prev</a></li>
<!-- 페이징 처리 필요 -->
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
</div>
<div class="modal fade" id="savePostsModal" tabindex="-1" role="dialog"
aria-labelledby="savePostsLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="savePostsLabel">게시글 등록</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="title">제목</label> <input type="text" class="form-control" id="title"
placeholder="제목을 입력하세요" />
</div>
<div class="form-group">
<label for="author"> 작성자 </label> <input type="text" class="form-control" id="author"
placeholder="작성자를 입력하세요" />
</div>
<div class="form-group">
<label for="content"> 내용 </label>
<textarea class="form-control" id="content" placeholder="내용을 입력하세요"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">취소</button>
<button type="button" class="btn btn-primary" id="btn-save">등록</button>
</div>
</div>
</div>
</div>
</section>
</html>
error.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{common/layout}">
<section layout:fragment="content">
<div class="container wrap__content">
<h1>Error</h1>
</div>
</section>
</html>
info.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{common/layout}">
<section layout:fragment="content">
<div class="container wrap__content">
<div class="row">
<div class="col-sm-6 col-md-8 col-md-offset-2">
<div class="thumbnail">
<div class="caption">
<h3>
<span sec:authentication="name"></span>
</h3>
<p>
<a href="#" class="btn btn-primary" role="button">Button</a> <a href="#"
class="btn btn-default" role="button">Button</a>
</p>
</div>
</div>
</div>
</div>
</div>
</section>
</html>
반응형
'스프링 부트 > 웹MVC' 카테고리의 다른 글
글 등록, 글 목록 불러오기, 해당 글의 상세 페이지로 이동하기 (0) | 2021.12.09 |
---|---|
업데이트, 더티 체킹 (0) | 2021.12.03 |
thymeleaf 적용 (0) | 2021.12.01 |
User 생성 작업 (0) | 2021.12.01 |
셋업 (0) | 2021.12.01 |