[Spring Boot] 05. 간단한 Security (시큐리티) 설정

💡 간단 개념 정리

PasswordEncoder
Spring Security에서 비밀번호를 안전하게 저장할 수 있도록 제공하는 인터페이스이다. 단방향 해쉬 알고리즘에 Salt를 추가하여 인코딩하는 방식을 사용한다.

FormLogin
MVC 방식에서 화면을 보여 주고 아이디와 비밀 번호를 입력하는 전통적인 로그인을 말한다.

CSRF (Cross-Site Request Forgery)
사이트 간 요청 위조를 뜻한다. 스프링 시큐리티에서는 @EnableWebSecurity 어노테이션을 이용해 CSRF를 방지하는 기능을 제공한다. 먼저 서버에서 임의의 토큰을 발급한다. 자원에 대한 변경 요청이 되돌아 올 경우, 토큰 값을 확인하여 클라이언트가 정상적인 요청을 보낸 것인지 확인한다. 만일 CSRF 토큰이 유효하지 않다면(값이 다르거나 수명이 끝났으면) 4nn 상태 코드를 리턴한다.

우선 수정 및 추가하게 될 파일을 먼저 살펴 보자. (🤗 표시 참고)

  1. New - Spring Starter Project - Spring Boot DevTools, Lombok, Spring Security, Thymeleaf, Spring Web 선택 후 프로젝트 생성

  2. 시큐리티 외에 기본적인 설정들을 잡아 주기
    ① 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>

  3. src/main/java - com.example.demo - ZboardApplication에 아래 내용 추가

    @Bean
    public PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    PasswordEncoder를 주입받아 사용하려면 @Bean으로 등록해 주어야 한다. ZboardApplication에는 이미 @SpringBootApplication 어노테이션이 등록되어 있기 때문에 @Configuration은 따로 등록하지 않아도 된다.

    스프링 시큐리티 5에서는 직접 PasswordEncoder을 생성하지 않고 스프링 시큐리티에 위임(Delegating)하도록 하는 것이 표준이다. 따라서 createDelegatingPasswordEncoder() 메소드를 사용한다.

  4. 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라는 이름이 붙는다.

  1. 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
    권한으로 메소드에 접근할 수 있는지를 설정

  1. 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>

  2. 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>

좋은 웹페이지 즐겨찾기