DelegatingFilterProxy
security的实现就是一系列的Filter,但是Filter是Servlet里面的标准定义,而Spring-security是在Spring里面玩的,Spring里面的Filter是交给Spring容器的,Servlet容器和Spring之间怎么关联起来呢?
Spring就提供了一个DelegatingFilterProxy,可以为Servlet容器的生命周期和Spring的ApplicationContext之间做个桥接,DelegatingFilterProxy
Spring提供了一个DelegatingFilterProxy的过滤器实现,它允许在Servlet容器的生命周期和Spring的ApplicationContext之间架桥。Servlet容器允许使用自己的标准注册Filter实例,但它不知道spring定义的bean。您可以通过标准Servlet容器机制注册DelegatingFilterProxy 这个Filter,但将所有工作委托给实现Filter的Spring Bean。
DelegatingFilterProxy 的作用图如下:它从ApplicationContext
中找Bean Filter0 并调用。
那个Bean Filter0 是懒加载初始化的,在调用DelegatingFilterProxy的doFilter的时候才会调用它的init方法。注意:它是在spring-web包里面的一个Filter。
FilterChainProxy
spring security 对Servlet的支持都包含在了FilterChainProxy。它是Spring security提供的一个Filter,它可以委派很多Filter实例到SecurityFilterChain(下面会讲述)中。FilterChainProxy是一个Bean,被包装在
DelegatingFilterProxy中,就相当于上述说的那个Bean Filter0。FilterChainProxy扮演的角色如下:注意:它是在spring-security-web包里面的一个Filter。
SecurityFilterChain
这个SecurityFilterChain在FilterChainProxy中是个集合,这个集合里面是一系列的Filter,对不同的请求会有不同的Filter会执行到。SecurityFilterChain的角色如下:
在这个集合里面的Filter都是Bean,他们维护在FilterChainProxy里面而不是DelegatingFilterProxy里面,是因为FilterChainProxy提供了一些高级特性可以让Servlet容器和DelegatingFilterProxy直接使用。首先,对于Spring security Servlet的项目,它提供了一个入口,如果想排查security的问题,可以从这里开始debug。
其次:FilterChainProxy 作为Spring security的核心,它可以处理一些必要的核心功能,比如清除SecurityContext避免内存泄露。还可以设置HttpFirewall保护应用收到某种攻击。
此外,它对于什么时候调用一个SecurityFilterChain提供了更多的灵活性。在Servlet容器中Filter实例是否被调用仅仅取决于URL。但是FilterChainProxy可以通过RequestMatcher
接口和入参HttpServletRequest,灵活的决定此Filer是否要被调用。
多个SecurityFilterChain的执行示意图如下:
FilterChainProxy决定哪一个SecurityFilterChain要被调用。只有第一个匹配上的SecurityFilterChain才会被调用。例如/api/message的一个请求过来,SecurityFilterChain0匹配上了就会被执行。后面的SecurityFilterChain虽然也能匹配上但是也不会被执行到了。
注意SecurityFilterChain0
只有三个security Filter实例,SecurityFilterChainn
只有四个
每一个SecurityFilterChain
可以是唯一的并且他们的配置都是互相隔离的。并且可以让SecurityFilterChain
里面没有Filter实例。
Security Filters
上面提到的SecurityFilterChain
有一个实现类DefaultSecurityFilterChain,它里面有一个List<Filter> filters,RequestMatcher requestMatcher属性,这些filters的用途可以是认证,授权,漏洞保护等。这个Filter会按照一定的顺序执行。Filter的顺序可以参见FilterOrderRegistration。
Printing the Security Filters
在INFO级别的日志里面,会把SecurityFilterChain里面
所有Filter打印出来。例如下图所示:
此外还可以通过配置把每次请求所执行的Filter打印出来,可以更直观的看出一次请求都执行了那些Filter,并在那一步的Filter失败了。如下是开启trace级别日志的情况下打印出来的日志信息。
添加自定义Filter到Security Filter Chain。
一般情况下,默认的security filter都能满足需求了,但是也支持添加自定义的Filter到Security Filter Chain中。
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
return http.build();
}
注意上面的这种添加方式第二个参数AuthorizationFilter,不是支持所有的Filter的,可以添加的位置可以参见FilterOrderRegistration里面的filterToOrder属性。
但是添加的方式需要注意下,如果把自定义的Filter用注解@Component或者在Configure中用@Bean的方式定义,Spring boot会自动注入它到内嵌容器中,可能会导致这个Filter被执行两次。一次是容器里面调用,一次是spring security里调用。
如果必须把一个Filter进行依赖注入,又要避免两次重复调用,可以通过定义FilterRegistrationBean
这个Bean设置enabled属性为false,
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
实现Filter,一般不建议直接实现Filter接口,建议继承 OncePerRequestFilter ,只需要重写doFilterInternal
即可。
异常处理
ExceptionTranslationFilter
是Security Filter链上的一个Filter, 用来处理AccessDeniedException and AuthenticationException 两类异常,如果不是这两类异常会不做任何处理直接抛出。流程图如下:
1)这个Filter会先放行,让后续的Filter执行,如果下游抛出了异常,在cache里面进行异常判断处理。
2)如果用户没有被认证或者抛出的异常是AuthenticationException,则开始认证流程。
在认证之间保存请求
当没有认证的情况下请求一个受保护的资源的时候,会抛出异常让上游认证,认证通过之后就会返回受保护的资源,在这个过程中涉及到要保存第一次来的时候request,因为认证之后要进行恢复。在Spring security中是用RequestCache的实现类来保存HttpServletRequest
。
认证
认证总体预览
SecurityContextHolder:存储被认证人的详细信息。
SecurityContext:SecurityContextHolder里面获取到认证信息和当前被认证的用户。
Authentication :传给AuthenticationManager
的入参,里面有用户提供的用于认证的凭证或者是SecurityContext里面获取到用户。
GrantedAuthority :被授予的权限信息在Authentication
里面。例如角色,scope等。
AuthenticationManager: 定义Spring Security的过滤器如何执行身份验证的API。
ProviderManager :AuthenticationManager的最常用的实现。
AuthenticationProvider:ProviderManager
用来处理某个具体类型的认证。
Request Credentials with AuthenticationEntryPoint :用于从客户端请求凭证(例如,重定向到登录页面,发送WWW-Authenticate响应等)。
AbstractAuthenticationProcessingFilter:用于认证的抽象Filter,从它里面可以看出来认证流程之间各部分是如何协同工作的。
SecurityContextHolder
SecurityContextHolder它是Spring security认证模型中的顶层类。它里面有SecurityContext
的维护。它里面存储了认证者的详细信息。Spring security不关心它是怎么被创建的,只要它里面有值,那么就认为当前用户被认证过了。
让一个用户成为一个被认证通过的用户最简单的方式就是直接设置SecurityContextHolder。
例如下面的代码:
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
1)使用SecurityContextHolder.createEmptyContext();创建一个新的context,而不是直接SecurityContextHolder.getContext().setAuthentication(authentication)
,是为了避免线程之间的竞争。
2)创建一个Authentication 的实例,spring security不关心context里面存储的是那个类型的authentication,上述用的TestingAuthenticationToken,因为它够简单。还有诸如:UsernamePasswordAuthenticationToken的实现类。
3)把context放入到SecurityContextHolder中,后续可用于授权。
获取被认证的用户信息,就通过SecurityContextHolder来获取。
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
默认情况下SecurityContextHolder
是用ThreadLocal来存储详细信息的,所以同一个线程内都能获取到SecurityContext
。
有些应用程序并不完全适合使用ThreadLocal,因为它们使用线程的特定方式。例如,Swing客户端可能希望Java虚拟机中的所有线程使用相同的安全上下文。可以在启动的时候配置SecurityContextHolder的存储策略。对于单机应用,可以使用SecurityContextHolder.MODE_GLOBAL
策略。其他应用程序如果也想在线程中使用同一个被认证过的信息,可以开启SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
.这个策略。
默认的存储策略SecurityContextHolder.MODE_THREADLOCAL
,可以通过系统配置来改变这个属性,或者使用SecurityContextHolder
中的静态方法。
SecurityContext
它是从SecurityContextHolder
中获取的安全上下文,它里面有一个 Authentication 属性。
Authentication
Authentication 接口在Spring Security中主要有两个作用:
1)用于AuthenticationManager的入参,用来存储用户提供的用于认证的凭证。如果是这种场景的话,isAuthenticated()返回false。
2)代表当前被认证过的用户,可以从SecurityContext.中获取到。
Authentication中包含以下信息:
principal: 标识用户,也就是用户的详细信息。如果使用username/password来认证。这个principal通常是个UserDetails实例。
credentials: 通常指的是password。在大部分场景下,这个内容在认证成功之后都会被清除,以免泄露。
authorities:这个GrantedAuthority实例是用户被授权的权限的抽象,例如:roles,scopes。
GrantedAuthority
这个GrantedAuthority实例是用户被授权的权限的抽象,例如:roles,scopes。
可以通过Authentication.getAuthorities()方法获取GrantedAuthority实例。这个方法返回了一个GrantedAuthority
对象的集合。一个对象就是一个被授予的权限。这些被授予的权限通常是角色。这些角色对应web权限,方法权限或一些domain对象权限。
当使用username/password用于认证的时候,GrantedAuthority
实例通常是由UserDetailService加载的。
AuthenticationManager
它定义了Spring security里面的那些Filter怎么进行进行认证的。然后由调用AuthenticationManager的控制器(即Spring Security的Filters实例)在SecurityContextHolder上设置返回的身份验证。如果不整合Spring Security的那些Filters,也可以直接使用SecurityContextHolder不用AuthenticationManager。
虽然AuthenticationManager的实现可以是任意的,但最常见的实现是ProviderManager。
ProviderManager
ProviderManager是AuthenticationManager一个最常用的实现。它把要处理的任务委派给一系列AuthenticationProvider实例(集合)。每一个AuthenticationProvider
可以处理当前认证是成功的还是失败的或者它自己处理不了就让一下AuthenticationProvider
决定。
如果一个AuthenticationProvider
可以进行认证,但是认证失败了,抛出了一个ProviderNotFoundException 异常,它是一个特殊的AuthenticationException
异常。它表示ProviderManager 对于入参Authentication并不支持。
实际上,每一个AuthenticationProvider
都对一个特定的认证类型进行处理的。例如,一个AuthenticationProvider
可以校验用户名密码,而另外一个则是认证SAML 断言的。这样对外暴露一个AuthenticationManager
bean,但是可以支持多种类型的认证。
ProviderManager还允许配置一个可选的父AuthenticationManager,它在没有AuthenticationProvider可以进行认证的情况下被访问。这个父类可以是任何类型的AuthenticationManager,但它通常是ProviderManager的一个实例。
实际上,多个ProviderManager
实例可能共享一个父AuthenticationManager,这在有多个SecurityFilterChain实例具有一些共同身份验证的场景中比较常见。但也有不同的身份验证机制(即不同的ProviderManager实例)。
默认情况下,在认证成功之后,ProviderManager
会尝试从Authentication
中清除掉敏感的认证信息(credentials)。这样避免让密码这种信息长时间保存在HttpSession中。
这种默认的清除机制当使用缓存user的时候,可能会导致问题。例如,在使用用户对象缓存以提高无状态应用程序中的性能时,这可能会导致问题。如果Authentication包含对缓存中的对象(例如UserDetails实例)的引用,并且它的凭据已被删除,则无法再根据缓存的值进行身份验证。如果要使用缓存,则需要考虑到这一点。一个解决方案是首先在缓存实现中或在创建返回的身份验证对象的AuthenticationProvider中创建对象的副本。或者,可以禁用ProviderManager上的eraseCredentialsAfterAuthentication属性。有关Javadoc类,请参阅Javadoc 。
AuthenticationProvider
可以往AuthenticationProvider 里注入多个 ProviderManager 。 每一个AuthenticationProvider都提供一种认证类型。例如: DaoAuthenticationProvider 支持基于用户名密码的认证。JwtAuthenticationProvider
支持认证一个JWT token。
Request Credentials with AuthenticationEntryPoint
AuthenticationEntryPoint用于发送从客户端请求凭据的HTTP响应。
有时候,客户端请求一个资源的时候会携带凭证(credentials: 比如用户名密码)。这种场景下
Spring security 不需要提供一个Http 响应,因为入参已经带了。
在其他情况下,客户端向未授权访问的资源发出未经身份验证的请求。在这种情况下,使用AuthenticationEntryPoint的实现从客户端请求凭据。AuthenticationEntryPoint实现可能会执行重定向到登录页面、使用WWW-Authenticate头响应或有其他操作。
AbstractAuthenticationProcessingFilter
authenticationprocessingfilter用作验证用户凭据的基本过滤器。在对凭证进行身份验证之前,Spring Security通常通过使用AuthenticationEntryPoint请求凭证。
authenticationprocessingfilter认证流程如下:
1)当用户提交他们的凭证的时候,AbstractAuthenticationProcessingFilter
从HttpServletRequest中获取入参构建Authentication用于认证。Authentication的类型和AbstractAuthenticationProcessingFilter
的子类有关。例如: UsernamePasswordAuthenticationFilter从HttpServletRequest中根据username/password构建一个UsernamePasswordAuthenticationToken。
2)把构建的Authentication传递给 AuthenticationManager进行认证。
3)如果认证失败。
(1)SecurityContextHolder清空。
(2)调用RememberMeServices.loginFail,如果remeber me没有被配置,这一步不做任务操作。参见rememberme 包。
(3)调用AuthenticationFailureHandler
,详细参见这个AuthenticationFailureHandler接口。
4)认证成功。
(1) SessionAuthenticationStrategy
会收到一个新的登录的通知。详细参见:SessionAuthenticationStrategy接口。
(2)SecurityContextHolder中设置Authentication.如果想在后续的请求中获取到SecurityContext(这是自动的)则需要显示调用SecurityContextRepository#saveContext
。
参见SecurityContextHolderFilter类。
(3)调用RememberMeServices.loginSuccess,如果没有配置remeber me,这一步不做任何操作。
(4)ApplicationEventPublisher
发布一个InteractiveAuthenticationSuccessEvent事件。
(5) 调用AuthenticationSuccessHandler
。参见AuthenticationSuccessHandler接口。