Spring security/01.Security

05. 프로젝트 구성과 초기 동작

  • -


이번 포스트에서는 전반적인 프로젝트 구성 및 환경 설정을 해보자.

 

문제링크

 

프로젝트 생성

다음과 같이 프로젝트를 생성해보자.

주요 특징은 Java17 기반의 Maven Project이고 Spring Boot DevTools, Lombok, Spring Web, Thymeleaf, Spring Security, H2 Database, Spring Data JPA를 사용한다.

 

application.yml 편집

spring data jpa와 h2, mustache 사용을 위한 yml 파일을 다음과 같이 작성해보자.

logging:
  level:
    root: info
  pattern:
    console: '%clr(%d{HH:mm:ss} [%-5p] [%c{20}.%M.%L] %m%n)'
spring:
  output:
    ansi:
      enabled: always
  jpa:
    hibernate:
      ddl-auto: validate # 실행 시 테이블 자동 생성 설정
  mustache:
    suffix: .html
    servlet:
      expose-session-attributes: true
      expose-request-attributes: true
  datasource:
    url: jdbc:h2:~/spring_security
    driver-class-name: org.h2.Driver
    username: doding
    password: 1234
server:
  servlet:
    encoding:
      force: true
---
spring:
  config:
    activate:
      on-profile:
        - dev
  jpa:
    hibernate:
      ddl-auto: create # 실행 시 테이블 자동 생성 설정
    properties:
      hibernate:
        '[format_sql]': true # 출력되는 sql을 보기 좋게 format 할 것인가?
    show-sql: true
  h2:
    console:
      enabled: true     # 웹 console을 통해 h2 db에 접속할 것인가?
      path: /h2-console # 웹 console의 접속 경로
logging:
  level:
    '[com.doding]': trace
    '[org.hibernate.orm.jdbc.bind]': trace
---
spring:
  config:
    activate:
      on-profile:
        - test
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=TRUE # 개별 테스트 종료 시 DB 자동 종료
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create # 실행 시 테이블 자동 생성
logging:
  level:
    '[com.doding]': trace
    '[org.hibernate.orm.jdbc.bind]': trace # 실행되는 sql에 전달되는 파라미터 출력

 

기본 controller와 index.html 작성

/ 요청을 처리하기 위한 controller와 index.html을 작성해보자.

package com.doding.example.controller;

public class HomeController {

    @GetMapping({"/", "."})
    public String index(Model model) {
        model.addAttribute("type", "Home");
        return "index";
    }
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>doding's</title>
  </head>
  <body>
    <h1>지금 타입: {{type}}</h1>

    <ul>
      <li><a href="/login">login</a></li>
      <li><a href="/secured/user">user</a></li>
      <li><a href="/secured/staff">staff</a></li>
      <li><a href="/secured/admin">admin</a></li>
      <li><a href="/logout">logout</a></li>
    </ul>
  </body>
</html>

 

동작 확인

애플리케이션을 실행시키면 단지 spring-security에 대한 의존성을 추가했다는 이유 하나만으로 자동 환경 설정이 이뤄진다. 결과로 console을 살펴보면 security password가 출력된다.

Using generated security password: e7534aa8-4ef3-4b04-b0e8-a8075c4ad16e

 

먼저 로그인을 해보자. 브라우저를 통해 localhost:8080을 요청하면 form 기반의 로그인 창이 나온다. username은 user, password는 위에서 생성된 password를 입력해준다.

결과로 위에서 만들었던 html이 출력되면 성공이다.

다음으로 logout을 하려면 localhost:8080/logout이라고 입력한다.

여기까지 동작하면 실습을 위한 기본적인 프로젝트 구성 및 점검은 끝이다.

 

How is it Possible?

 

기본 설정

Spring Security는 단지 spring-boot-starter-security를 의존성에 추가했다는 이유 만으로 다음과 같은 auto configuration을 적용해준다.

@EnableWebSecurity   // 1
@Configuration
public class DefaultSecurityConfig {
    @Bean
    @ConditionalOnMissingBean(UserDetailsService.class)  // 2
    InMemoryUserDetailsManager inMemoryUserDetailsManager() {
        String generatedPassword = // ...;
        return new InMemoryUserDetailsManager(User.withUsername("user")
                .password(generatedPassword).roles("USER").build());
    }

    @Bean
    @ConditionalOnMissingBean(AuthenticationEventPublisher.class)  // 3
    DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher delegate) { 
        return new DefaultAuthenticationEventPublisher(delegate);
    }
}
  1. @EnableWebSecurity: 이 애너테이션은 Spring Security의 Filter Chain을 @Bean으로 등록해준다.
  2. InMemoryUserDetailsManager는 UserDetailsService 타입의 빈으로 inmemory 기반으로 사용자명은 user, 비밀번호는 generatedPassword를 이용하도록 구성된다. 참고로 이 빈이 없으면 @ConditionalOnMissingBean에 의해 대체 빈으로 UserDetailsService를 찾는다.
  3. 인증과 관련된 이벤트 처리를 위해 AuthenticationEventPublisher를 빈으로 구성한다.

 

@EnableWebSecurity

Spring Security의 설정 파일에는 @EnableWebSecurity를 선언해준다.

@Target(ElementType.TYPE)
@Documented
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
		HttpSecurityConfiguration.class })
@EnableGlobalAuthentication
public @interface EnableWebSecurity {
	boolean debug() default false;
}

@EnableWebSecurity는 SecurityFilterChain 빈등 Security와 관련된 빈들을 노출시켜 SpringSecurity를 구성할 수 있게 한다. 

이제 @EnableWebSecurity가 선언된 클래스에 여러가지 Security 관련 빈들을 선언하면서 Spring Security가 동작하게 된다.

  • UserDetailService: 인증을 위한 사용자 설정을 담당한다.
  • SecurityFilterChain: 인증 절차에 관련된 설정으로 적용 대상, 권한, 로그인, 로그아웃 형태들을 설정한다.
  • WebSecurityCustomizer: SecurityFilterChain과 관련된 설정을 진행하며 주로 제외할 경로를 설정한다.

우리도 사용자 정의의 설정을 위해 SecurityConfig 파일을 생성하도록 하자. 그냥 만들면 심심하니까 계층적 권한 관리를 위해 RoleHierarchy 빈을 추가해주자.

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
    @Bean // 계층적 권한 부여를 위한 static bean 구성
    static RoleHierarchy roleHierarchy() {
        return RoleHierarchyImpl.withDefaultRolePrefix()
                .role("ADMIN").implies("STAFF")
                .role("STAFF").implies("USER")
                .role("USER").implies("GUEST")
                .build();
    }
}

 

기본 구성 요소 추가

본격적인 학습을 하기 전에 먼저 테스트에 사용할 controller들과 페이지를 만들어보자.

 

Controller

HomeController에 /user, /staff, /admin, /accessdenied를 처리하기 위한 메서드를 추가한다.

@Controller
@RequestMapping("/secured")
public class SecuredController {
    @GetMapping("/user")
    public String user(Model model) {
        model.addAttribute("type", "user");
        return "index";
    }
    @GetMapping("/admin")
    public String admin(Model model) {
        model.addAttribute("type", "admin");
        return "index";
    }
    @GetMapping("/staff")
    public String manager(Model model) {
        model.addAttribute("type", "staff");
        return "index";
    }
    @GetMapping("/staff")
    public String manager(Model model) {
        model.addAttribute("type", "staff");
        return "index";
    }
    @GetMapping("/access-denied")
    public String accessdenied(Model model) {
        model.addAttribute("type", "accessdenied");
        return "index";
    }
}

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.