본문 바로가기

스프링 부트/웹MVC

스프링 시큐리티 적용

반응형

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">&times;</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">&times;</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