본문 바로가기
공부방

[TIL] 스프링 시큐리티 Quick Start

by hseong 2023. 8. 5.

1. 스프링 시큐리티

스프링 시큐리티는 인증, 권한 부여, 악의적인 공격으로부터 보호하기 위한 포괄적 지원을 제공하는 프레임워크입니다. 공식 문서에서는 스프링 기반 애플리케이션에서 보안을 위한 사실상(de-facto) 표준이라고 표현하고 있습니다.

스프링 시큐리티가 제공하는 기능은 다음 세 가지로 요약할 수 있습니다.

1) 인증(Authentication)

스프링 시큐리티는 사용자 인증을 위한 지원을 제공합니다. 가장 일반적인 방법은 사용자의 아이디와 패스워드를 입력하도록 요청하는 것입니다. 인증이 완료되면 신원을 파악하고 권한 부여를 수행할 수 있습니다.

2) 악의적인 공격으로부터 보호(Protection Against Exploits)

일반적으로 알려진 악의적 공격으로부터 보호 기능을 제공합니다.

3) 여러 프레임워크와의 통합(Integration)

수많은 프레임워크 및 API와의 통합을 통해 편리하게 이용 가능합니다.

2. Quick Start

의존성 추가

스프링 시큐리티 역시 다른 스프링 프로젝트와 동일하게 스타터 의존성을 제공합니다. 다음 의존성을 build.gradle에 추가하기만 하면 곧바로 시큐리티를 적용할 수 있습니다.

implementation 'org.springframework.boot:spring-boot-starter-security'

Configuration

스프링 시큐리티를 접한지 얼마 되지 않아 과거에는 어땠는지 모르겠습니다. 다만 최근 1, 2년 사이 설정 정보를 등록하는 방식이 조금씩 변하고 있습니다.

만약 설정 정보에 대한 참고가 필요하시면 공식 문서를 이용하시는게 가장 좋은 방법이라고 생각합니다. 구버전과의 최신 버전의 비교를 통한 설명은 스프링 블로그를 통해 확인하실 수 있습니다.

과거에는 WebSecurityConfigurerAdapter를 상속하여 메서드 오버라이딩을 통해 설정 정보를 조작할 수 있었습니다.

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
    }

}

스프링 시큐리티 5.7 버전 부터는 WebSecurityConfigurerAdapter이 deprecated 되어 개발자가 직접 bean으로 등록하는 방식으로 변경되었습니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/blog/**").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(formLogin -> formLogin
                        .loginPage("/login")
                        .permitAll()
                )
                .rememberMe(Customizer.withDefaults());

        return http.build();
    }
}

공식 문서에 따르면 스프링 시큐리티 7 버전 부터는 메서드 체이닝만을 이용한 설정 방식은 더 이상 지원하지 않는다고 합니다. 따라서 다음과 같은 lambda DSL 스타일을 통해 설정 정보를 작성해야 합니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/blog/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(formLogin -> formLogin
                .loginPage("/login")
                .permitAll()
            )
            .rememberMe(Customizer.withDefaults());

        return http.build();
    }
}
  • @EnbleWebSecurity: @Configuration 클래스에 해당 어노테이션을 추가하면 SecurityFilterChain 빈을 등록하여 세부적인 스프링 보안 구성을 정의할 수 있습니다.
  • SecurityFilterChain: 스프링 시큐리티에서 다양한 보안 설정을 등록하기 위한 구성 클래스입니다.
  • HttpSecrity: 클래스는 세부적인 웹 보안 기능 설정을 처리할 수 있는 API를 제공합니다.
    • 요청에 대한 인가, 폼 로그인, 리멤버 미, 로그아웃 등 다양한 보안 기능 설정을 제공합니다.

기본 로그인 계정

기본 UserDetailsService는 단일 사용자를 가집니다. 사용자 이름은 user이고 비밀번호는 애플리케이션이 시작되는 시점에 다음과 같이 출력됩니다.

Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35

기본 로그인 계정은 application.yml 파일에서 관리할 수 있습니다. 물론 실제 프로젝트에서 이런식으로 관리해서는 안 된다고 합니다.

spring:
  security:
    user:
      name: user
      password: user123
      role: USER

웹 애플리케이션에서는 다음과 같은 사항이 기본적으로 적용된다고 합니다.

  • 인 메모리 저장소와 단일 유저를 가지는 UserDetailsService
  • 요청의 Accpet 헤더에 따라 폼 기반 로그인 또는 HTTP Basic security
  • 인증 이벤트 발행을 위한 DefaultAuthenticationEventPublisher

타임 리프 확장

다음 의존성을 추가하면 Thymeleaf view 에서 스프링 시큐리티 관련 기능을 쉽게 사용 가능합니다.

implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

view 에는 다음과 같은 네임스페이스를 추가해주면 됩니다.

<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

이제 sec을 접두사로 가지는 dialect를 통해 view 에서 스프링 시큐리티 관련 기능을 이용할 수 있습니다.

Authentication 객체는 다음과 같이 접근 가능합니다.

<div th:text="${#authentication.name}">
  The value of the "name" property of the authentication object should appear here.
</div>

Authorization 객체도 유사한 방법으로 접근 가능합니다. 일반적으로 th:ifth:unless 태그와 함께 사용된다고 합니다.

<div th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}">
  This will only be displayed if authenticated user has role ROLE_ADMIN.
</div>

작동하나..?

이제 잘 작동하는지 직접 확인해보겠습니다. 우선 security 설정 정보를 다음과 같이 수정해줍니다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/hello").authenticated()  //(1)
                    .anyRequest().permitAll()  //(2)
            )
            .formLogin(formLogin -> formLogin
                    .defaultSuccessUrl("/")  //(3)
                    .permitAll()  //(4)
            );
    return http.build();
  }

  @Bean
  public UserDetailsService userDetailsService() {
    UserDetails user = User.builder()  //(5)
            .username("user")
            .password("{noop}user123")
            .roles("USER")
            .build();
    UserDetails admin = User.builder()  //(6)
            .username("admin")
            .password("{noop}admin123")
            .roles("ADMIN")
            .build();
    return new InMemoryUserDetailsManager(user, admin);
  }
}

  • (1): /hello로 들어오는 모든 요청은 인증되어야 합니다.
  • (2): 그 외의 모든 요청은 허용합니다.
  • (3): 로그인 페이지에 직접 접근하여 로그인 성공시 /로 리다이렉트시킵니다.
  • (4): 로그인 페이지, 로그인 프로세스에 대한 모든 접근을 허용합니다.
  • (5): 테스트용 유저를 추가합니다. 스프링 부트는 DelegatingPasswordEncoder을 기본 PasswordEncoder로 등록합니다. 따라서 평문의 비밀번호를 사용하기 위해서는 {noop}와 같은 접두사를 붙여줘야 합니다.
  • (6): 테스트용 어드민을 추가합니다. ROLE_ADMIN에 대한 접근 제어를 테스트하기 위한 계정입니다.

타임리프 뷰에서 Authentication 객체를 잘 가져오는지 확인하기 위해 다음과 같은 뷰를 추가해주고, 컨틀롤러를 만들어줍니다.

resources/template/hello.html

<!doctype html>
<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div th:text="${#authentication.name}">
    The value of the "name" property of the authentication object should appear here.
</div>
<div th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}">
    This will only be displayed if authenticated user has role ROLE_ADMIN.
</div>
</body>
</html>

src/../HelloController.class

@Controller
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

이제 프로젝트를 실행시키고 /hello로 접근하면 다음과 같이 스프링 시큐리티가 기본 제공하는 로그인 페이지를 만날 수 있습니다.

@Configuration 클래스에서 등록한 유저 계정을 입력하면 다음과 같이 유저의 계정명이 출력되는 것을 확인할 수 있습니다.

어드민 계정을 입력한다면 역할에 따른 문자열도 잘 출력되는 것을 확인할 수 있습니다.



참조

프로그래머스 데브코스 백엔드

Spring Security Docs

Spring Security without the WebSecurityConfigurerAdapter

thymeleaf-extras-springsecurity