[Spring Boot] 05. 간단한 Security (시큐리티) 설정
💡 간단 개념 정리
PasswordEncoder
Spring Security에서 비밀번호를 안전하게 저장할 수 있도록 제공하는 인터페이스이다. 단방향 해쉬 알고리즘에 Salt를 추가하여 인코딩하는 방식을 사용한다.
FormLogin
MVC 방식에서 화면을 보여 주고 아이디와 비밀 번호를 입력하는 전통적인 로그인을 말한다.
CSRF (Cross-Site Request Forgery)
사이트 간 요청 위조를 뜻한다. 스프링 시큐리티에서는 @EnableWebSecurity 어노테이션을 이용해 CSRF를 방지하는 기능을 제공한다. 먼저 서버에서 임의의 토큰을 발급한다. 자원에 대한 변경 요청이 되돌아 올 경우, 토큰 값을 확인하여 클라이언트가 정상적인 요청을 보낸 것인지 확인한다. 만일 CSRF 토큰이 유효하지 않다면(값이 다르거나 수명이 끝났으면) 4nn 상태 코드를 리턴한다.
우선 수정 및 추가하게 될 파일을 먼저 살펴 보자. (🤗 표시 참고)
-
New - Spring Starter Project - Spring Boot DevTools, Lombok, Spring Security, Thymeleaf, Spring Web 선택 후 프로젝트 생성
-
시큐리티 외에 기본적인 설정들을 잡아 주기
① src/main/resources - application.properties에서 서버 포트 잡아 주기server.port=8081
② src/main/resources -logback-spring.xml
추가<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> %d{HH:mm:ss.SSS} %highlight(%-5level) %magenta(%-4relative) --- [ %thread{10} ] %cyan(%logger{40}) : %msg%n </pattern> </encoder> </appender> <!-- 내가 만든 클래스에 대한 로깅 설정 --> <logger name="com.example" level="info" /> <!-- 3rd party 로깅 설정 --> <logger name="org.springframework" level="info" /> <logger name="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" level="trace" /> <!-- log4jdbc 로깅 설정 --> <logger name="jdbc.connection" level="warn"/> <logger name="jdbc.resultsettable" level="info"/> <logger name="jdbc.audit" level="warn"/> <logger name="jdbc.sqltiming" level="warn"/> <logger name="jdbc.resultset" level="warn"/> <root level="info"> <appender-ref ref="console" /> </root> </configuration>
③pom.xml
에 타임리프에서 스프링 시큐리티를 사용하기 위한 의존성 주입<!-- 타임리프에서 스프링 시큐리티를 사용하기 위한 라이브러리 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
-
src/main/java - com.example.demo -
ZboardApplication
에 아래 내용 추가@Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
PasswordEncoder를 주입받아 사용하려면 @Bean으로 등록해 주어야 한다.
ZboardApplication
에는 이미 @SpringBootApplication 어노테이션이 등록되어 있기 때문에 @Configuration은 따로 등록하지 않아도 된다.
스프링 시큐리티 5에서는 직접 PasswordEncoder을 생성하지 않고 스프링 시큐리티에 위임(Delegating)하도록 하는 것이 표준이다. 따라서createDelegatingPasswordEncoder()
메소드를 사용한다.
-
src/main/java - com.example.demo -
SecurityConfig
생성package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; // 스프링 시큐리티에 대한 일반 설정 @Override protected void configure(HttpSecurity http) throws Exception { // 로그인에 대한 설정 http.formLogin().loginPage("/sample/login").loginProcessingUrl("/sample/login") .usernameParameter("username").passwordParameter("password") .defaultSuccessUrl("/").failureUrl("/sample/login?error") // 권한 오류에 대한 설정 .and() .exceptionHandling().accessDeniedPage("/sample/error") // 로그아웃에 대한 설정 .and() .logout().logoutUrl("/sample/logout").logoutSuccessUrl("/"); } // 사용자 아이디, 비밀 번호, 권한 등을 관리하는 AuthenticationManager(인증 매니저) 객체에 대한 설정 // 아직 DB와 연결하지 않았기 때문에 시험용으로 테스트용 유저를 담은 것이다. @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("spring").password(passwordEncoder.encode("1234")).roles("USER") .and() .withUser("system").password(passwordEncoder.encode("1234")).roles("ADMIN") .and() .withUser("admin").password(passwordEncoder.encode("1234")).roles("USER", "ADMIN"); } }
@EnableWebSecurity
와
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
는 시큐리티 설정을 위해 들어가게 되는 어노테이션들이다.public class SecurityConfig extends WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter
는 스프링 시큐리티 설정을 기본 구현한 중간 단계의 추상 클래스이다.📝 스프링 시큐리티 설정 파일이라면, 스프링 시큐리티 표준 인터페이스를 상속(implements)해야 한다. 그런데 인터페이스를 implements하려면 모든 추상 메소드를 다 구현해야 한다. 그래서 메소드를 기본 구현한 중간 클래스를 두는 경우가 많다. 이러한 중간 단계의 클래스에 Adapter라는 이름이 붙는다.
-
src/main/java - com.example.demo.controller -
SampleController
생성package com.example.demo.controller; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class SampleController { @PreAuthorize("isAnonymous()") @GetMapping("/sample/login") public void login() { } // 누구나 접근 가능한 루트 페이지이기 때문에 권한에 대한 어노테이션이 없다. @GetMapping({"/", "/sample/list"}) public String list() { return "sample/list"; } @Secured("ROLE_USER") @GetMapping("/sample/user") public void user() { } @Secured("ROLE_ADMIN") @GetMapping("/sample/admin") public void admin() { } @PreAuthorize("isAuthenticated()") @GetMapping("/sample/authenticated") public void authenticated() { } @PreAuthorize("isAnonymous()") @GetMapping(value = "/sample/anonymous") public void anonymous() { } @GetMapping("/sample/error") public void error403() { } }
@PreAuthorize, @PostAuthorize
로그인 여부로 메소드에 접근할 수 있는지를 설정
@Secured
권한으로 메소드에 접근할 수 있는지를 설정
-
src/main/resources - templates - fragments -
header.html
,nav.html
,footer.html
생성<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <h1>헤더 페이지</h1> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <title>Insert title here</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <script> $(function() { $('#login').click(function() { location.href = "/sample/login"; }) $('#logout').click(function() { const $form = $('<form>').attr('action', '/sample/logout').attr('method', 'post').appendTo($('body')); // $('<input>').attr('type', 'hidden').attr('name', '_csrf').val('${_csrf.token}').appendTo($form); $('<input>').attr('type', 'hidden').attr('name', '_csrf').val($('#csrf').text()).appendTo($form); $form.submit(); }) }) </script> </head> <body> <span th:text='${_csrf.token}' id="csrf"></span> <!-- 로그인했으면 로그아웃 버튼 표시, 안 했으면 로그인 버튼 출력 --> <button sec:authorize="isAnonymous()" id="login">로그인</button> <button sec:authorize="isAuthenticated()" id="logout">로그아웃</button> <!-- 권한 표시 --> <div sec:authorize="hasRole('ADMIN')">관리자</div> <div sec:authorize="hasRole('USER')">일반 유저</div> </body> </html>
📝 폼(form)은 블록 요소로, 디자인을 안 좋은 쪽으로 변형시킬 수 있기 때문에 자바스크립트로 넣어 준다.
<span th:text='${_csrf.token}' id="csrf">
은 id를 이용해$('#csrf').text()
로 가지고 와야 한다. 타임리프 문법인'${_csrf.token}'
로 가지고 오게 되면 자바스크립트는 그냥 문자열로 인식한다.<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <h3>푸터 페이지</h3> </body> </html>
-
src/main/resources - templates - sample -
admin.html
,anonymous.html
,authenticated.html
,error.html
,list.html
,login.html
,user.html
생성<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <div id="page"> <header th:replace="/fragments/header"> </header> <nav th:replace="/fragments/nav"> </nav> <section> 관리자만 접근 가능 ˚✧₊⁎( ˘ω˘ )⁎⁺˳✧༚ </section> <footer th:replace="/fragments/footer"> </footer> </div> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <div id="page"> <header th:replace="/fragments/header"> </header> <nav th:replace="/fragments/nav"> </nav> <section> 비로그인 유저만 접근 가능! (*ૂ❛ᴗ❛*ૂ) </section> <footer th:replace="/fragments/footer"> </footer> </div> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <div id="page"> <header th:replace="/fragments/header"> </header> <nav th:replace="/fragments/nav"> </nav> <section> 로그인한 유저만 접근 가능! (*ૂ❛ᴗ❛*ૂ) </section> <footer th:replace="/fragments/footer"> </footer> </div> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <div id="page"> <header th:replace="/fragments/header"> </header> <nav th:replace="/fragments/nav"> </nav> <section> <p style="color:red">잘못된 접근입니다. (403 오류)</p> <a href="/">루트 페이지로 이동</a> </section> <footer th:replace="/fragments/footer"> </footer> </div> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <div id="page"> <header th:replace="/fragments/header"> </header> <nav th:replace="/fragments/nav"> </nav> <section> (〜^∇^)〜 누구나 접근 가능합니다! 〜(^∇^〜)<br> <a href="/sample/anonymous">🔲 비로그인 접근 가능</a><br> <a href="/sample/authenticated">🔲 로그인만 접근 가능</a><br> <a href="/sample/admin">🔲 관리자만 접근 가능</a><br> <a href="/sample/user">🔲 일반 유저만 접근 가능</a> </section> <footer th:replace="/fragments/footer"> </footer> </div> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <div id="page"> <header th:replace="/fragments/header"> </header> <nav th:replace="/fragments/nav"> </nav> <section> <form action="/sample/login" method="post"> 아이디 <input type="text" name="username"><br> 비밀 번호 <input type="password" name="password" value="1234"><br> <input type="hidden" name="_csrf" th:value="${_csrf.token}"> <button>로그인</button> </form> </section> <footer th:replace="/fragments/footer"> </footer> </div> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>Insert title here</title> </head> <body> <div id="page"> <header th:replace="/fragments/header"> </header> <nav th:replace="/fragments/nav"> </nav> <section> 일반 유저 권한만 접근 가능 ๑◕‿‿◕๑ </section> <footer th:replace="/fragments/footer"> </footer> </div> </body> </html>
Author And Source
이 문제에 관하여([Spring Boot] 05. 간단한 Security (시큐리티) 설정), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@haramiee/Spring-Boot-05저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)