Spring Security
新版springboot 3.2已经集成Spring Security 6.2,和以前会有一些变化,本文主要针对官网的文档进行一些个人翻译和个人理解,不对地方请指正。
整体架构
Spring Security的Servlet 支持是基于Servelet过滤器,如下图所示,当http请求到来时候,会经历下面的过滤器。
客户端发送请求到应用程序,容器会生成过滤链(其中包含过滤器实例)并且会根据URL地址处理httpServletRequest。在Spring MVC应用程序中,Servlet是DispatcherServlet的实例,一个Servlet最多可以处理一个HttpServletRequest 和 HttpServletResponse. 但是使用的过滤器不止一个,主要有以下两点:
1.防止下游过滤器实例或者Servlet被调用,这种情况下,过滤器通常会写入HttpServletResponse.
2.修改HttpServletRequest 和 HttpServletRespons,并且传递给下游过滤器和Servlet.
过滤器强大在于传递给它的过滤器链。
过滤器链的例子:
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
}
官网这段话,其实看这个图或者说和servlet的过滤器执行顺序相关,图里所示就是Filter0->Filter1->Filter2->Servlet,所以使用过滤器是多个的,并且要按照过滤器顺序往下传递,每个过滤器最重要是重写doFilter, 并且这个函数参数有过滤器链FilterChain,每个过滤器在做完自己事情(do something before)后,就得传递给过滤器链,chain.doFilter,保证往下传递。相反,回来的时候肯定相反方向:Servelet->Filter2->Filter1->Filter0。
DelegatingFilterProxy
Spring提供了一个过滤器叫DelegatingFilterProxy,它链接了Servlet容器的生命周期和Spring applicationContext。Servlet容器有自己的标准,可以注册Filter过滤器实例,但是它没有办法找到Spring所定义的Beans对象。你可以注册DelegatingFilterProxy,并且它符合Servlet容器机制,还能委托给Spring Bean所实现的Filter过滤器。
DelegatingFilterProxy会从AllicationContext查找Bean Filter0并且调用Bean Filter0过滤器
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName); //1
delegate.doFilter(request, response); //2
}
1.惰性获取已经注册Spring Bean的Filter
2.将工作委托给Spring Bean
DelegatingFilterProxy另个好处就是允许延迟查询Filter实例。这个是很重要的,因为容器需要在容器启动前注册Filter实例,但是,Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,直到需要注册 Filter 实例之后才会执行此操作。
FilterChainProxy
Spring Security的Servelet 支持都被包含在了FilterChainProxy,FilterChainProxy是一个特殊过滤器,能够通过SecurityFilterChain委托给许多Filter过滤器实例,FilterChainProxy是一个bean,所以通常被包装在DelegatingFilterProxy中。
SecurityFilterChain
SecurityFilterChain被FilterChainProxy使用,并且决定了哪个Spring Security过滤器实例对当前请求进行调用
SecurityFilterChain 中的SecurityFilter通常是 Bean,但它们是使用 FilterChainProxy 而不是 DelegatingFilterProxy 注册的。FilterChainProxy 为直接向 Servlet 容器或 DelegatingFilterProxy 注册提供了许多优势。首先,它为 Spring Security 的所有 Servlet 支持提供了一个起点。因此,如果您尝试对 Spring Security 的 Servlet 支持进行故障排除,在 FilterChainProxy 中添加调试点是一个很好的起点。
其次,由于 FilterChainProxy 是 Spring Security 使用的核心,因此它可以执行不被视为可选的任务。例如,它会清除 SecurityContext 以避免内存泄漏。它还应用 Spring Security 的 HttpFirewall 来保护应用程序免受某些类型的攻击。
此外,它还在确定何时应调用 SecurityFilterChain 方面提供了更大的灵活性。在 Servlet 容器中,仅根据 URL 调用过滤器实例。但是,FilterChainProxy 可以使用 RequestMatcher 接口根据 HttpServletRequest 中的任何内容确定调用。
在多个 SecurityFilterChain 图中,FilterChainProxy 决定应使用哪个 SecurityFilterChain。仅调用匹配的第一个 SecurityFilterChain。如果请求 /api/messages/ 的 URL,则它首先与 /api/** 的 SecurityFilterChain0 模式匹配,因此仅调用 SecurityFilterChain0,即使它在 SecurityFilterChainn 上也匹配。如果请求 /messages/ 的 URL,则它与 /api/** 的 SecurityFilterChain0 模式不匹配,因此 FilterChainProxy 会继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例匹配,则调用 SecurityFilterChaann。
请注意,SecurityFilterChain0 只配置了三个security Filter实例。但是,SecurityFilterChainn 配置了四个安全筛选器实例。需要注意的是,每个 SecurityFilterChain 都可以是唯一的,并且可以单独配置。事实上,如果应用程序希望 Spring Security 忽略某些请求,则 SecurityFilterChain 可能具有零安全过滤器实例。
SecurityFilter
security Filters使用 SecurityFilterChain API 插入到 FilterChainProxy 中。这些过滤器可用于多种不同的目的,例如身份验证、授权、漏洞利用保护等。过滤器按特定顺序执行,以保证在正确的时间调用它们,例如,执行身份验证的过滤器应先调用,然后再调用执行授权的过滤器。通常没有必要知道 Spring Security 过滤器的顺序。但是,有时了解排序是有益的,如果您想了解它们,可以查看 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 | Added by |
---|---|
| |
| |
| |
|
1.调用 CsrfFilter 来防止 CSRF 攻击。
2.调用身份验证筛选器来对请求进行身份验证。
3.调用 AuthorizationFilter 来授权请求。
添加自定义的过滤器
大多数情况下,默认安全筛选器足以为应用程序提供安全性。但是,有时您可能希望将自定义筛选器添加到安全筛选器链。
例如,假设你要添加一个获取租户 ID 标头的筛选器,并检查当前用户是否有权访问该租户。前面的描述已经为我们提供了在何处添加过滤器的线索,因为我们需要知道当前用户,因此我们需要在身份验证过滤器之后添加它。
首先,让我们创建过滤器:
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
}
}
面的示例代码执行以下操作:
1.从请求标头中获取租户 ID。
2.检查当前用户是否有权访问租户 ID。
3.如果用户具有访问权限,则调用链中的其余筛选器。
4.如果用户没有访问权限,则引发 AccessDeniedException。
官网还给了一个建议,可以从 OncePerRequestFilter 进行扩展,而不是实现 Filter,OncePerRequestFilter 是每个请求仅调用一次的过滤器的基类,并提供带有 HttpServletRequest 和 HttpServletResponse 参数的 doFilterInternal 方法。
这个建议,主要是因为servlet filter因为版本不同,以及不同web container,一个请求不一定只过一个fitler,像servlet2.3中,Filter会经过一切请求,包括服务器内部使用forward.
最后在把上面自定义的过滤器加到过滤器链中.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
return http.build();
}
HttpSecurity会通过addFilterBefore在AuthorizationFilter前添加TenanFilter.
通过在 AuthorizationFilter 之前添加过滤器,我们可以确保在身份验证过滤器之后调用 TenantFilter。您还可以使用 HttpSecurity的addFilterAfter 在特定过滤器之后添加过滤器,或使用 HttpSecurity的addFilterAt 在过滤器链中的特定过滤器位置添加过滤器。
将过滤器声明为 Spring Bean 时要小心,不管用 @Component 注解或在配置中将其声明为 Bean,因为 Spring Boot 会自动将其注册到嵌入式容器中。这可能会导致过滤器被调用两次,一次由容器调用,一次由 Spring Security 调用,并且顺序不同。
例如,如果您仍然希望将过滤器声明为 Spring Bean 以利用依赖注入,并避免重复调用,您可以通过声明 FilterRegistrationBean bean 并将其 enabled 属性设置为 false 来告诉 Spring Boot 不要将其注册到容器中:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
处理异常
ExceptionTranslationFilter 允许将 AccessDeniedException 和 AuthenticationException 转换为 HTTP 响应。
ExceptionTranslationFilter 作为安全过滤器之一插入到 FilterChainProxy 中。
1.ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response) 来调用应用程序的其余部分。
2.如果用户未通过身份验证或它是 AuthenticationException,则启动Authentication。(1)SecurityContextHolder 被清除。
(2)保存 HttpServletRequest,以便在Authentication验证成功后使用它来重播原始请求。
(3)AuthenticationEntryPoint 用于从客户端请求凭据。例如,它可能会重定向到登录页面或发送 WWW-Authenticate 标头。
3.否则,如果它是 AccessDeniedException,则拒绝访问。调用 AccessDeniedHandler 来处理被拒绝的访问。
如果应用程序未引发 AccessDeniedException 或 AuthenticationException,则 ExceptionTranslationFilter 不会执行任何操作。
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
1. 调用 FilterChain.doFilter(request, response) 等同于调用应用程序的其余部分。这意味着,如果应用程序的另一部分(FilterSecurityInterceptor 或方法安全性)引发 AuthenticationException 或 AccessDeniedException,则会在此处捕获并处理它。
2.如果用户未经过身份验证或是 AuthenticationException,则启动身份验证。
3.否则,访问被拒绝
在Authentication中保持请求
如处理安全异常中所示,当请求没有身份验证并且针对需要身份验证的资源时,需要保存身份验证资源的请求,以便在身份验证成功后重新请求。在 Spring Security 中,这是通过使用 RequestCache 实现保存 HttpServletRequest 来完成的。
RequestCache
HttpServletRequest 保存在 RequestCache 中。当用户成功进行身份验证时,RequestCache 将用于重播原始请求。RequestCacheAwareFilter 是使用 RequestCache 保存 HttpServletRequest 的。
默认情况下,使用 HttpSessionRequestCache。下面的代码演示如何自定义 RequestCache 实现,该实现用于检查 HttpSession 中是否存在名为 continue 的参数时已保存的请求。
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
防止请求被保存
您可能希望不要在会话中存储用户未经身份验证的请求,原因有很多。您可能希望将该存储卸载到用户的浏览器上,或将其存储在数据库中。或者,您可能希望关闭此功能,因为您始终希望将用户重定向到主页,而不是他们在登录前尝试访问的页面。
为此,可以使用 NullRequestCache 实现。
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
Spring Boot Security Get started
@EnableWebSecurity //1
@Configuration
public class DefaultSecurityConfig {
@Bean
@ConditionalOnMissingBean(UserDetailsService.class)
InMemoryUserDetailsManager inMemoryUserDetailsManager() { //2
String generatedPassword = // ...;
return new InMemoryUserDetailsManager(User.withUsername("user")
.password(generatedPassword).roles("ROLE_USER").build());
}
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher delegate) { //3
return new DefaultAuthenticationEventPublisher(delegate);
}
}
1.使用EnableWebSecurity注册Spring Security's 注册默认的过滤链(default filter chain)对应的bean
2.第二个就是注册一个UserDetailsService默认bean,现在注册是默认的内存里面用户和密码
3.注册一个authenticationEventPublisher的bean对象给authentication 事件