整体结构
Spring Security整体是基于Servlet的Filter机制。简单来说,其就是将一系列的Filter组合起来,然后建立一定的机制来管理这些Filter;并且为特定的Filter提供一定的配套类来达成一定的功能,比如认证的Filter,CSRF的Filter等。
下面是对其管理机制的一些简单介绍
FilterChain
Servlet的Filter的执行过程,类似于函数的调用过程。
即当一个请求从客户端发送到Server的时候,在到达对应的Servlet处理之前,都会调用提前的设定的对应Filter。并且这些Filter会按照调用栈执行,并且会按照调用的顺序再返回,由第一个Filter来执行response。
其流程图如下:
值得注意的是,SpringMVC就是基于Servelt的,其基本原理为,建立一个名为DispatcherServlet
的servlet来接受所有的请求,然后再在SpringMVC应用内为其映射对应的Controller请求函数。
而上面整个过程中用的Filter就构成了一个链式的结构,其又被称为Filter链(FilterChain
)。
DelegatingFilterProxy
这个类实际上是为Spring上下文和Servlet之间提供了一个桥梁。即一般来说,我们为Servlet注册上下文的时候,Spring上下文并不一定启动了,所以在Filter中无法感知到Spring应用,并且这些Filter也是用户自己new出来的,可以说是违背了Spring应用中IOC的核心,即把类的构造权交由Spring容器来做,而不是自己手动新建。
所以该类就是为了解决这个问题,从名字就可以看出,这是一个具有代理功能的类。即一个DelegatingFilterProxy
代理一个真实的Filter,并且被代理的这个类是在Spring容器中的。代理类中提供了从容器中查找的对应真实Filter的方法:
1 | protected void initFilterBean() throws ServletException { |
和调用真实Filter的方法:
1 | protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { |
所以一般的用法就是,在Spring容器中定义对应的Filter,然后再向Servlet容器中注册一个自己新建的DelegatingFilterProxy
类实例,该实例就会自动查找并调用Spring容器中的Filter。
如新建的DelegatingFilterProxy
的doFilter方法:
1 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { |
使用这种代理机制的另一个好处就是延迟加载,实际上还是克服了无法将容器对象注册为标准Filter的缺点。因为Filter需要在应用启动的时候就注册,但此时Spring容器可能并没有初始化好,那么就是无法进行注册的。则借用代理,在需要时加载,那么在需要的时候一般就已经初始化好了(因为请求都发送过来了)。
反思
实际上该类很简单,一共就180行代码。
这里其实也为我们经常遇到的一个问题提供了解决思路,即:在Filter中如果需要使用Spring容器中的对象,该怎么拿到?或者更宽泛一点:在Spring容器可能还没有初始化完成时(还比如interceptor种),我们又需要使用容器中的对象,该怎么使用?一个典型的场景就是:在Filter中需要使用redis,除非自己手写一个RedisTemplate,但Spring-Redis实际上已经新建好并且注入到Spring容器中,只需要我们取出来就好了,这种情况下,自己再去新建就显得没有必要了。
那么这里我们就可以使用其中的API,通过request来获取对应的Spring上下文,并以此来获取容器的对象来进行使用。值得注意的是,这里的使用也是延迟的,即获取和使用都是在请求到来时才进行,因为容器的初始化在之后才进行的。(例如,如果将上下文的获取,容器中对象中的获取放到Filter的构造器中,那就肯定得不到对应的对象,因为那时容器还没有初始化,获取的必定为null。)
一个简单的实例如下(从request获取存储在Spring上下文中的RedisUtils对象):
1 | public static RedisUtils getInstance(HttpServletRequest request){ |
这里主要是使用WebApplicationContextUtils
工具类。
再看一下其是如何实现的:
1 |
|
可以看到其实逻辑很简单,就是从ServletContext
通过名字获取WebApplicationContext
对象。而且个对象是在应用启动的时候由Spring放进去的。
FilterChainProxy
FilterChainProxy
实际上相当于Spring Security的DelegatingFilterProxy
。即它也是一个代理类,但它与DelegatingFilterProxy
不同的点在于:
FilterChainProxy
是所有经过Spring Security请求的入口位置,所以其代理的是多个Filter,而不是像DelegatingFilterProxy
一样,仅仅代理一个Filter。FilterChainProxy
作为Spring Security的核心Filter,其可以用来做一些其他任务,比如清除SecurityContext
防止内存泄露。FilterChainProxy
提供了更灵活的Filter匹配机制,即哪些Filter将在哪些路径中触发。
简单来说,就是DelegatingFilterProxy
代理了FilterChainProxy
,而FilterChainProxy
又代理一系列的Filter(这些代理就是普通的Spring容器内的对象了)。
下面是一个简单的例子:
- Servlet内看到的Filter,可以看到第四个就是一个
DelegatingFilterProxy
,这个类就是Spring Security注册的。
- 可以看到,这个
DelegatingFilterProxy
代理了一个FilterChainProxy
对象。
- 该
FilterChainProxy
又代理了一系列的Filter。
所以实际上就是一个套娃的过程。
Spring Security中的默认拦截器顺序
ForceEagerSessionCreationFilter
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter
认证流程
SecurityContextHolder
SecurityContextHolder
是Spring认证的核心。它存储了当前被认证的用户信息,其存储的是一个SecurityContext
对象。
Spring Security并不关心如何存储该信息,但是它包含一个值,它被用作当前通过身份验证的用户。
其结构如下:
表示用户通过身份验证的最简单方法是直接设置SecurityContextHolder:
1 | SecurityContext context = SecurityContextHolder.createEmptyContext(); |
获取存储的信息:
1 | SecurityContext context = SecurityContextHolder.getContext(); |
默认情况下,SecurityContextHolder
使用ThreadLocal
来存储这些细节,这意味着SecurityContext
总是对同一个线程中的方法可用,即使SecurityContext
没有显式地作为参数传递给那些方法。如果在处理当前主体的请求后清除线程,那么以这种方式使用ThreadLocal
是非常安全的。Spring Security的FilterChainProxy
确保SecurityContext
可以被清除掉。
在其他的场景中,可能并不适合使用ThreadLocal
,比如Swing客户端中,可能希望Java虚拟机中的所有线程使用相同的SecurityContextHolder
。
那么就可以设定SecurityContextHolder
的策略,即设置SecurityContextHolder.strategyName
。其有4种模式:
- public static final String MODE_THREADLOCAL = “MODE_THREADLOCAL”:默认,即ThreadLocal方式。
- public static final String MODE_INHERITABLETHREADLOCAL = “MODE_INHERITABLETHREADLOCAL”:其所有派生线程共享。
- public static final String MODE_GLOBAL = “MODE_GLOBAL”:所有线程共享上下文。
SecurityContext
其是一个接口,只定义了基本的设置Authentication
的方法。但其有一个实现类SecurityContextImpl
,但是实现也很简单,只是定义了最基本的Authentication
成员变量的get和set方法。该类是实际存储用户数据的类,其将会被存储到SecurityContextHolder
中。
Authentication
这是一个接口,其含义是真正存储用户认证信息的类,其源代码如下:
1 | // |
可以看到很简单,其中的getCredentials
是定义的获取用户的的密钥,getPrincipal
用来获取用户的名字,而getDetails
用于获取其他附加的信息(可由自己定义)。
由于其是一个接口,必须要有其实现类才能工作,所以在Spring Security内部先定义了一个AbstractAuthenticationToken
抽象类实现了一定的功能。
然后又为不同场景中定义了很多不同的Authentication
,这些Authentication
都被命名为xxxToken。
最常见的就是UsernamePasswordAuthenticationToken
和AnonymousAuthenticationToken
。
其中AnonymousAuthenticationToken
是用来存储账户密码的对象。
其实现很简单,即四十多行代码,其中principal就是用户名,credentials就是密码:
1 | // |
该对象会交给对应类型的ProviderManager
来进行认证,并且是更具这个类的类型来选择ProviderManager
的类型。
GrantedAuthority
这个类是用来存储用户的权限实例。比如角色和权限。
一般是从Authentication.getAuthorities()
获取。这个对象是对应于特定角色的权限。一般来说是在UserDetailService
中装配User对象时一起装配的(比如是从数据库中的RBAC模型获取的)。
AuthenticationManager
AuthenticationManager
是一个接口,它规定了Spring Security如何进行验证用户。但实际上其只定义了一个authenticate
方法,这个方法也是整个认证过程的关键。
我们可以自己实现AuthenticationManager
,但一般都是使用Spring Security自己实现的ProviderManager
。
ProviderManager
ProviderManager
是Spring Security认证的核心类。它是AuthenticationManager
的最常见实现类。
它代理一系列的AuthenticationProvider
,AuthenticationProvider
是实际进行用户认证的类。每个AuthenticationProvider
都有机会表明身份验证应该成功、失败,或者表明它不能做出决定并允许下游的AuthenticationProvider
做出决定。如果没有配置AuthenticationProvider
实例可以验证,与ProviderNotFoundException
认证失败,这是一个特殊AuthenticationException
表明ProviderManager
没有配置为支持传递给它的身份验证类型(Authentication
类型)。
简单来说就是由ProviderManager
代理一系列的AuthenticationProvider
,然后当认证请求到来的时候(通常是在Filter中),来调用对应类型的AuthenticationProvider
来进行用户的验证。
其结构如下:
另外,ProviderManager
还允许配置一个可选的父类AuthenticationManager
(当然一般也是ProviderManager
)。在当前ProviderManager
找不到对应AuthenticationProvider
的情况下就会调用父类进行验证。
AuthenticationProvider
这个类也是一个十分重要的类,它提供了对不同类型Authentication
的具体验证方案。Spring Security也提供了很多具体的验证方案,在Spring Security中,用户密码验证的默认AuthenticationProvider
是DaoAuthenticationProvider
,它实现了AbstractUserDetailsAuthenticationProvider
类。
其中主要规定了密码加密方式setPasswordEncoder
,UserDetailService
的获取等方法。
而我们要自定义自己的验证方式时,也可以自定义AuthenticationProvider
。主要就是要实现authenticate
方法。
例如,下面是一个实现邮箱登录的AuthenticationProvider
:
1 | public class EmailCodeAuthenticationProvider implements AuthenticationProvider { |
UserDetailsService
该接口一般用在AuthenticationProvider
类中用来获取用户的完整信息,该接口定义了loadUserByUsername
方法用来获取用户的信息。但是如果在自定义的AuthenticationProvider
中,就不一定要用实现UserDetailsService
的方式来获取用户信息了,也可以直接定义自己的类。(基本的问题就是比如邮箱登录的时候,我们应当是通过邮箱来获取用户,而不是用户名)。