Spring Security Getting Start 和 整体架构

203 阅读8分钟

Getting Started

SpringBoot Maven项目添加Spring Security依赖。

$ curl -i http://localhost:8080/some/path
HTTP/1.1 401
...

就会返回401 Unauthorized

Tip 如果你在浏览器中请求该URL,你会看到一个默认的登录页。


如果加上验证信息,即用户名为user,密码为console中打印的generated security password:。

$ curl -i -u user:8e557245-73e2-4286-969a-ff57fe326336 http://localhost:8080/some/path
HTTP/1.1 404
...

就可以访问到该接口,这里的404是由于我们未定义该接口。


默认行为

这里我们将介绍Spring Security Starter所提供的默认行为:

下面是Spring Security Starter自动配置的简化示例,来方便说明

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

    @Bean
    @ConditionalOnMissingBean(AuthenticationEventPublisher.class)
    DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher delegate) {
        return new DefaultAuthenticationEventPublisher(delegate);
    }
}

添加了@EnableWebSecurity注解(将Spring Security默认的 filter chain 注册为了bean)

创建了UserDetailsService bean,创建了一个usernameuser以及一个随机生成的password,该password会在console中打印出来

创建了一个AuthenticationEventPublisher bean用来发布authentication事件

Note

只要将Filter声明为bean,spring Boot就会将其加入到应用的filter chain中。这意味着,在Spring Boot使用@EnableWebSecurity注解会自动注册Spring Security的filter chain。


Spring Security的架构

Spring Security在Servlet的实现是基于Servlet Filter。所以先大体浏览下Filter。下面图片展示了对于单个HTTP请求的电影处理分层。

filterchain

Client发送请求到应用,容器会创建一条FilterChain,其中包含Filter实例和用来处理HttpServletRequestServlet。在Spring MVC应用中,ServletDispatcherService的实例,只有一个Servlet来处理一个HttpServletRequest和一个HttpServletResponse。但是可以使用多个Filter

  • 避免下游的FilterServlet被执行。对于这种情况,Filter通常会写入HttpServletResponse
  • 修改下游FilterServlet所用到的 HttpServletRequest or HttpServletResponse

FilterChain Usage Example

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
}

由于Filter只影响下游的FilterServlet,所以Filter的顺序非常重要。

DelegatingFilterProxy

Spring提供一个名为DelegatingFilterProxyFilter实现,该Filter在Servlet 容器的生命周期和Spring的ApplicationContext之间建立了桥梁。因为普通Servlet容器的Filter访问不到Spring容器的bean。你可以注册DelegatingFilterProxy到标准的Servlet容器中,然后实际逻辑转交给实现Filter的Spring Bean。

下图是DelegatingFilterProxy如何融入进Filter 实例和FilterChain的。

delegatingfilterproxy

DelegatingFilterProxy会从ApplicationContext中寻找Bean Filter0,并执行它。下面就是这段逻辑的伪代码

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	Filter delegate = getFilterBean(someBeanName);
	delegate.doFilter(request, response);
}

FilterChainProxy

Spring Security对Servlet支持是通过FilterChainProxy实现的。FilterChainProxy是一个由Spring Security提供的Filter,可以让SecurityFilterChain委托多个Filter。由于FilterChainProxy是一个Bean,所以它通常会被DelegatingFilterProxy包装。

filterchainproxy

SecurityFilterChain

SecurityFilterChain用来确定当前request应该调用哪些Spring Security Filter。

securityfilterchain

SecurityFilterChain中的Security Filter通常是一些bean,但他们通过FilterChainProxy注册而不是通过DelegatingFilterProxy。使用FilterChainProxy的优点是:

  • 它是Spring Security Servlet的入口,所以当你对Spring Security 对 servlet的支持有疑问时,可以打个断点从这里排查
  • 因为FilterChainProxy是Spring Security使用的核心,它可以执行类似清除SecurityContext来避免内存泄漏的任务,它还可以应用Spring Security的HttpFireWall来保护应用免受一些攻击。
  • 在确定应该执行哪条SecurityFilterChain上,它提供了非常大的灵活性。在Servlet容器中,Filter是否执行取决于URL。然而,FilterChainProxy可以根据HttpServletRequest中的任何东西来进行选择

下图展示了多个SecurityFilterChain的情况

multi securityfilterchain

在上图中,FilterChainProxy来决定应该用哪个SecurityFilterChain。注意 只有第一个匹配的SecurityFilterChain会被执行。比如,请求的url为/api/message,第一个匹配的SecurityFilterChain即chain0会被执行。如果请求的url为/message,chain n就会被执行。

请注意SecurityFilterChain 0只有三个Security Filter实例,SecurityFilterChain n有4个Filter实例。理解 每个chain的配置都是隔离的非常重要。实际上,SecurityFilterChain可以不配置Filter,这样应用就可以让Spring Security忽略特定request。

Security FIlters

Security Filters是通过SecuritFilterChain Api添加到 FIlterChainProxy中的。这些Filter用来实现认证 授权 防范攻击的作用,并且这些Filter按照特定的顺序执行来保证他们在正确的时间被调用,认证的Filter需要再授权的Filter执行之前调用。通常情况你不需要知道Spring Security的Filter的顺序,需要时可查阅FilterOrderRegistration

如下例

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

}

上面的配置会产生Filter的执行顺序如下面所示

FilterAdded by
CsrfFilterHttpSecurity#csrf
UsernamePasswordAuthenticationFilterHttpSecurity#formLogin
BasicAuthenticationFilterHttpSecurity#httpBasic
AuthorizationFilterHttpSecurity#authorizeHttpRequests

Note

除了上边的Filter,还有其他的一些Filter,如果你想看一个请求执行了哪些Filter,你可以打印出来


Printing the Security Filters

Filter的列表会在应用启动的时候打印出来,log层级为INFO。你可以在console中看到下面的输出

2023-06-14T08:55:22.321-03:00  INFO 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]

这样你就能看到为每个filter chain的Security Filter。

你可以配置你的应用来为每个请求打印每个filter的执行,这在当你检查你添加的filter是否在请求中执行时非常有用。

logging.level.org.springframework.security=TRACE

Adding a Custom Filter to the Filter Chain

大多数情况下,默认的Security Filter能为你的应用提供足够的安全保障。但Spring Security也提供了加入自定义Filter的功能。

例如,想加一个filter 可以获取tanant id header,并检查当前用户是否有访问这个tenant的权限。通过上面的描述,我们应该知道大概需要将该Filter放在哪个位置上,因为我们需要知道当前的登录用户,所以我们需要把它加载认证的filter之后。

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;

public class TenantFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String tenantId = request.getHeader("X-Tenant-Id"); (1)
        boolean hasAccess = isUserAllowed(tenantId); (2)
        if (hasAccess) {
            filterChain.doFilter(request, response); (3)
            return;
        }
        throw new AccessDeniedException("Access denied"); (4)
    }

}

Tip

除了实现Filter,你还继承OncePerRequestFilter,该基类对每次请求只执行一次,还提供了有 HttpServletRequestHttpServletResponse参数的doFilterInternal方法。


如下面代码所示,将Filter添加到Security filter chain中

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
    return http.build();
}

通过在AuthorizationFilter前添加该Filter,我们确保了TenantFilter在认证filter后执行。

当你声明你的filter为Spring bean时,无论是通过@Component注解还是在configuration中声明为bean,此时你就需要当心了,因为这样Spring Boot会自动将其注册到Servlet容器中,这会导致Filter执行两次,一次由容器执行,一次由Spring Security执行。

但如果就是想自定义的filter注册为bean,来使用spring 的依赖注入等特性,还想避免重复执行,你可以让spring boot不将该filter注册到容器中,通过声明一个FilterRegistrationBean并设置其enable属性为false:

@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}

Handling Security Exception

ExceptionTranslationFilter可以将AccessDeniedExceptionAuthenticationException转为HTTP响应。

下图展示了ExceptionTranslationFilter和其他组件之间的关系

exceptiontranslationfilter

  1. 首先ExceptionTranslationFilter会调用FilterChain.doFilter(request, response)来执行应用的剩余部分

  2. 如果用户未认证或者是AuthenticationException,就开始认证流程

    • 清除 SecurityContextHolder

    • 保存HttpServletRequest以便它可以用来重放原始的请求,当认证成功时

    • AuthenticationEntryPoint 用来请求client的认证信息,例如,重定向到一个login page,或者发送一个 WWW-Authenticate header

  3. 要么就是AccessDeniedException,那么就会调用AccessDeniedHandler来处理访问拒绝的事件

Note

如果应用没抛出AccessDeniedException或者AuthenticationException,那么ExceptionTranslationFilter就什么都没做。


ExceptionTranslationFilter的伪代码如下所示

try {
	filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication();
	} else {
		accessDenied();
	}
}

Saving Request Between Authentication

如在Handling Security Exceptions中介绍的,当一个请求未认证且访问的资源需要认证时,需要将该request保存以便在认证成功后进行再次请求。在Spring Security中,这是通过使用RequestCache的实现来完成保存的

RequestCache

HttpServletRequest是保存在RequestCache中。当用户认证成功时,RequestCache用来重放原始的请求。RequestCacheAwareFilter使用RequestCache来获取已保存的HttpServletRequest 在认证之后,ExceptionTranslationFilter使用RequestCache来保存HttpServletRequest 在检测到AuthenticationException。

默认情况下,使用的是HttpSessionRequestCache,下面展示了如果自定义Request实现只有参数有continue出现是 才会检查HttpSession来获取保存的request。

@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
	HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
	requestCache.setMatchingRequestParameterName("continue");
	http
		// ...
		.requestCache((cache) -> cache
			.requestCache(requestCache)
		);
	return http.build();
}

Prevent the Request From Being Saved

可能因为一些原因,你不想存储用户的未认证请求,此时你可以使用 NullRequestCache

@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    RequestCache nullRequestCache = new NullRequestCache();
    http
        // ...
        .requestCache((cache) -> cache
            .requestCache(nullRequestCache)
        );
    return http.build();
}