개발/Java|Spring

Spring Security 개념, 아키텍처, 필터 순서, 예제

달리초이 2023. 1. 17. 17:23


[목차]

1. Spring Security 개념

2. 아키텍처

3. 필터 순서

4. 샘플예제


스프링 시큐리티는 한 번에 이해가 쉽지 않다. 지금도 마찬가지지만, 공부하는 마음으로 조금씩 정리해 본다. 참고로 문서는 Spring Security 5.7.6 버전 기준이며, Java 8 이상의 런타임 환경이 필요하다. Spring Boot with Gradle 환경이다. 스프링 부트는 스프링 시큐리티 관련 의존성을 모두 묶어 'spring-boot-starter-security' 라는 스타터를 제공한다. 스타터는 스프링 이니셜라이저를 사용하면 쉽고 간단히 만들 수 있고, 아래처럼 수동으로 gradle 설정을 추가할 수도 있다.

dependencies {
    compile "org.springframework.boot:spring-boot-starter-security"
}

버전을 오버라이드하고 싶다면 버전을 명시하면 된다.

ext['spring-security.version']='5.3.2.RELEASE'

 

 

1. Spring Security 개념

스프링 시큐리티는 인증(Authentication)과 인가(Authorization), 그리고 전반적인 보안을 제공하는 스프링 프레임워크 중 하나이며, 스프링 생태계에서 사실상 표준이다.

 

인증(Authentication)과 인가(Authorization)

- 인증(Authentication)

특정 리소스에 액세스하려는 사용자의 ID를 확인하는 방법이다. 일반적인 예로 로그인 과정은 사용자 Username과 Password를 이용해서 인증을 처리하거나 SNS 로그인을 이용하여 인증을 위임하는 경우도 있다. 인증된 사용자 정보는 Session을 통해 관리하거나 토큰을 만들어 관리하는 방법을 이용한다.

 

- 인가(Authorization)

인증을 통해 사용자가 누구인지 확인 후 현재 사용자가 특정 대상(URL, 기능 등)을 사용(접근)할 권한이 있는지 검사하는 것이다. 권한이 있는지 없는지를 판단하여 허가를 해주는 과정이라고 볼 수 있다. 보통 'ROLE_' 접두어가 붙은 권한을 부여받는다.

 

이처럼 권한이 있는 사용자에게만 특정 페이지나 리소스에 접근을 허용하도록 해야 하는 경우가 대부분인데, 이때 인증과 인가, 보안 관련 설정을 쉽게 할 수 있도록 도와주는 게 바로 스프링 시큐리티 프레임워크이다.

 

Spring Boot Auto Configuration

스프링 부트가 자동으로 해주는 게 있다.

- 스프링 시큐리티의 디폴트 설정을 활성화해서 springSecurityFilterChain이라는 이름의 서블릿 Filter 빈을 생성한다. 이 빈이 어플리케이션 내의 모든 보안 처리를 담당한다.

- user라는 사용자 이름과 콘솔에 찍히는 랜덤 생성한 비밀번호를 가지고 있는 UserDetailsService 빈을 만든다.

- 서블릿 컨테이너에 springSecurityFilterChain이란 이름의 Filter 빈을 등록해 모든 요청에 적용한다. 

 

 

 

2. 아키텍처

Spring Security는 표준 서블릿 필터로 서블릿 컨테이너와 통합된다. 서플릿 필터를 기반으로 서블릿을 지원하므로 먼저 일반적인 필터의 역할을 살펴보면 좀 더 이해하기 쉽다. 다음 그림은 단일 HTTP 요청에 대한 핸들러의 일반적인 계층화를 보여 준다. 필터는 체인처럼 연결되어 있기 때문에 필터 체인이라고도 불리는데, 모든 요청은 이 필터들을 거쳐야만 서블릿에 도달할 수 있다.

 

 

@docs.spring.io

- FilterChain 사용 예

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	// do something before the rest of the application
    chain.doFilter(request, response); // invoke the rest of the application
    // do something after the rest of the application
}

 

그래서 스프링 시큐리티는 DelegatingFilterProxy라는 필터를 만들어 메인 필터체인에 끼워 넣는다. 그리고 하위에 시큐리티 필터 체인 그룹을 등록한다.

 

 

@docs.spring.io

 

- DelegatingFilterProxy 의사 코드

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	// Lazily get Filter that was registered as a Spring Bean
	// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
	Filter delegate = getFilterBean(someBeanName);
	// delegate work to the Spring Bean
	delegate.doFilter(request, response);
}

DelegatingFilterProxy는 ApplicationContext에서 Bean Filter0를 찾아 실행한다.

 

url 패턴에 따라 적용되는 필터체인을 다르게 설정할 수 있다. 즉 SecurityFilterChain은 고유하고 격리된 설정을 가질 수 있다. 만인에게 공개된 web resource의 경우에는 이 패턴을 따르더라도 필터를 무시하고 통과시키도록 설정하기도 한다. 이 필터체인에는 다양한 필터들이 있다. 필터는 넣거나 뺄 수 있고 순서를 조절할 수 있다. 하지만 기본적인 필터들은 그 순서가 어느정도 정해져 있어 무작정 변경했다간 치명적일 수도 있다는 점에 유의하자. 다음은 전체 스프링 시큐리티 필터의 순서이다. 

 

 

 

 

3. 필터 순서

  • ForceEagerSessionCreationFilter
  • ChannelProcessingFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter - Http 헤더를 검사한다.
  • CorsFilter - 허가된 사이트나 클라이언트의 요청인가?
  • CsrfFilter  - 내가 내보낸 리소스에서 올라온 요청인가?
  • LogoutFilter - 로그아웃 할 건가?
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter - username / password 로 로그인을 하려고 하는지? 만약 로그인이면 여기서 처리하고 가야 할 페이지로 보내준다.
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter - 여기저기서 로그인 하는 걸 허용할 것인가?
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter - Authorization 헤ㅐ더에 Bearer 토큰이 오면 인증 처리 해준다.
  • BasicAuthenticationFilter - Authorization 헤더에 Basic 토큰을 주면 검사해서 인증처리 해준다.
  • RequestCacheAwareFilter - 방금 요청한 request 이력이 다음에 필요할 수 있으니 캐시에 담아놓는다.
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter - 아직 Authentication 인증이 안된 경우라면 RememberMe 쿠키를 검사해서 인증 처리해준다.
  • AnonymousAuthenticationFilter - 마지막까지 인증이 안되었다면 익명 사용자로 처리한다.
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter - 서버에서 지정한 세션정책을 검사한다.
  • ExceptionTranslationFilter - 나 이후에 인증이나 권한 예외가 발생하면 잡아서 처리해준다.
  • FilterSecurityInterceptor - 여기까지 통과해서 왔다면 인증이 있다는 거니, 접속하려는 request에 들어갈 자격이 있는지 그리고 리턴한 결과를 너에게 보내줘도 되는건지 마지막으로 점검한다.
  • SwitchUserFilter

 

 

 

 

 

4. 샘플예제

5.5.x 버전부터 6.0.x 버전까지의 샘플 코드는 아래 repository에서 브런치로 나눠져 있어 쉽게 찾을 수 있다.

 

https://github.com/spring-projects/spring-security-samples

 

GitHub - spring-projects/spring-security-samples

Contribute to spring-projects/spring-security-samples development by creating an account on GitHub.

github.com

 

 

참고로 Spring Security 5.7.0-M2 버전부터는 WebSecurityConfigurerAdapter가 Deprecated 되어 SecurityFilterChain를 Bean으로 등록해서 사용해야 한다. 또 Configuring WebSecurity 또한 WebSecurityCustomizer를 bean으로 등록해서 사용한다. 참고로 Spring Security 5.4에서 SecurityFilterChain Bean을 생성하여 HttpSecurity를 구성하는 기능을 도입했다.

변경된 방법 참고 : Spring Security without the WebSecurityConfigurerAdapter

 

- 이전 코드(HTTP Basic으로 모든 엔드포인트를 보호하는 WebSecurityConfigurerAdapter를 사용한 구성 예)

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

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

}

 

- 권장 코드(SecurityFilterChain Bean을 생성하여 HttpSecurity를 사용한 구성 예)

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

}

 

 

 

 

Hello Security (without Spring MVC) 에서의 샘플코드

전체 코드를 볼 수 있는 Github Repository :

https://github.com/spring-projects/spring-security-samples/tree/5.7.x/servlet/java-configuration/hello-mvc-security/src/main/java/example

 

 

- build.gradle

plugins {
	id "java"
	id "nebula.integtest" version "8.2.0"
	id "org.gretty" version "3.0.6"
	id "war"
}

apply from: "gradle/gretty.gradle"

repositories {
	mavenCentral()
	maven { url "https://repo.spring.io/milestone" }
	maven { url "https://repo.spring.io/snapshot" }
}

dependencies {
	implementation platform("org.springframework:spring-framework-bom:5.3.13")
	implementation platform("org.springframework.security:spring-security-bom:5.7.0-SNAPSHOT")
	implementation platform("org.junit:junit-bom:5.7.0")

	implementation "org.springframework.security:spring-security-config"
	implementation "org.springframework.security:spring-security-web"
	implementation "org.springframework:spring-webmvc"
	implementation "org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE"

	testImplementation "org.assertj:assertj-core:3.18.0"
	testImplementation "org.springframework:spring-test"
	testImplementation "org.springframework.security:spring-security-test"
	testImplementation("org.junit.jupiter:junit-jupiter-api")

	testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")

	integTestImplementation "org.seleniumhq.selenium:htmlunit-driver:2.44.0"
}

tasks.withType(Test).configureEach {
	useJUnitPlatform()
	outputs.upToDateWhen { false }
}

 

- SecurityConfiguration.java

/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package example;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@EnableWebSecurity
public class SecurityConfiguration {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		// @formatter:off
		http
				.authorizeHttpRequests((authorize) -> authorize
						.anyRequest().authenticated()
				)
				.httpBasic(withDefaults())
				.formLogin(withDefaults());
		// @formatter:on
		return http.build();
	}

	// @formatter:off
	@Bean
	public UserDetailsService userDetailsService() {
		UserDetails user = User.withDefaultPasswordEncoder()
				.username("user")
				.password("password")
				.roles("USER")
				.build();
		return new InMemoryUserDetailsManager(user);
	}
	// @formatter:on

}

 

- SecurityWebApplicationInitializer.java

/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package example;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
import org.springframework.security.web.session.HttpSessionEventPublisher;

/**
 * We customize {@link AbstractSecurityWebApplicationInitializer} to enable the
 * {@link HttpSessionEventPublisher}.
 *
 * @author Rob Winch
 */
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

	@Override
	protected boolean enableHttpSessionEventPublisher() {
		return true;
	}

}

 

 

 

 

[참고]

Spring Security docs : https://docs.spring.io/spring-security/reference/5.8/index.html

최범균 : https://www.slideshare.net/madvirus/ss-36809454

치즈윤 : https://github.com/cheese10yun/TIL/blob/master/Spring/security/basic.md

옥탑방개발자 : https://github.com/jongwon/sp-fastcampus-spring-sec

 

728x90
반응형