一文带你快速走进
Spring Security的世界(本文是基于Servlet应用实现的Spring Security)。
Security架构
Filter
对基于Servlet的应用,Spring Security是通过其Filter(过滤器)来实现的。在应用程序接收到Servlet请求后,容器就会创建一个FilterChain(包含Filter和Servlet)来进行处理,其中Filter(过滤器)可以阻止请求进一步向下执行或者对请求的参数/返回值进行修改,Servlet则会在所有Filter通过后进行业务处理。下面给出了关于FilterChain的核心流程:
+----------+
| Client |
+----+-----+
FilterCain |
+-----------------------------+
| +---------v-----------+ |
| | Filter0 | |
| +---------+-----------+ |
| | |
| +---------v-----------+ |
| | Filter1 | |
| +---------+-----------+ |
| | |
| +---------v-----------+ |
| | FilterN | |
| +---------+-----------+ |
| | |
| +---------v-----------+ |
| | 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
}
Bean Filter
虽然Servlet容器允许以它标准的方式来注册Filter,但是这样却不能识别到Spring所定义的Bean。为了实现这一点,Spring提供了一个Filter实现类DelegatingFilterProxy,它会将Servlet容器的生命周期与Spring的ApplicationContext桥接在一起,即DelegatingFilterProxy可以通过Servlet容器标准的方式进行注册,同时也能够将所有的工作委托给Spring中实现了Filter的Bean。这样,FilterChain的核心流程就演变成这样:
+----------+
| Client |
+----+-----+
FilterCain |
+-----------------------------+
| +---------v-----------+ |
| | Filter0 | |
| +---------+-----------+ |
| | |
| +----------v------------+ |
| | DelegatingFilterProxy | |
| | +--------------+ | |
| | | Bean Filter0 | | |
| | +--------------+ | |
| +-----------------------+ |
| | |
| +---------v-----------+ |
| | FilterN | |
| +---------+-----------+ |
| | |
| +---------v-----------+ |
| | Servlet | |
| +---------------------+ |
+-----------------------------+
其中,DelegatingFilterProxy会从ApplicationContext中查询并调用Bean Filter0,伪代码如下所示:
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);
}
对于Bean Filter最典型的实现就是FilterChainProxy,它允许将处理工作通过SecurityFilterChain委托给多个Filter实例。这样,FilterChain的核心流程就演变成这样:
+----------+
| Client |
+----+-----+
FilterCain |
+---------------------------------------+ SecurityFilterChain
| +---------v-----------+ | +----------------------------+
| | Filter0 | | | |
| +---------+-----------+ | | +------------------+ |
| | | | | Security Filter0 | |
| | | | +--------+---------+ |
| +--------------v----------------+ | | | |
| | DelegatingFilterProxy | | | +--------+ |
| | +---------------------------+ | | | |--------| |
| | | FilterChainProxy +------------>+ |--------| |
| | +---------------------------+ | | | |--------| |
| +-------------------------------+ | | |--------| |
| | | | +--------+ |
| | | | | |
| +---------v-----------+ | | +--------v---------+ |
| | FilterN | | | | Security FilterN | |
| +---------+-----------+ | | +------------------+ |
| | | | |
| | | +----------------------------+
| +---------v-----------+ |
| | Servlet | |
| +---------------------+ |
+---------------------------------------+
其中,在SecurityFilterChain中的Security Filter通常是Bean,但是它们是被注册到FilterChainProxy而不是DelegatingFilterProxy。
相比于直接注册到
Servlet容器或者DelegatingFilterProxy,FilterChainProxy具有如下优势:
- 首先,它为所有对
Spring Security的Servlet支持提供了一个起始点。因此,如果你正在尝试对Spring Security的Servlet支持进行故障排除,那么可以在FilterChainProxy中添加一个调试点。- 其次,由于
FilterChainProxy是Spring Security使用的核心,我们可以在它上面执行一些必须的任务。例如,它清除SecurityContext以避免内存泄露。另外,它还可以应用Spring Security的HttpFirewall来保护应用程序免受某些类型的攻击。- 最后,它在确定何时应该调用
SecurityFilterChain方面上提供了更大的灵活性。相比于在Servlet容器中仅能根据URL来判断是否调用过滤器,FilterChainProxy则可以通过RequestMatcher接口来判断HttpServletRequest中的任何内容以确定是否调用过滤器。
另一方面,FilterChainProxy可以配置多个SecurityFilterChain,并通过配置来决定哪个SecurityFilterChain应该被使用。也就说FilterChainProxy可以让应用程序不同部分应用不同且完全独立的配置。这样,FilterChain的核心流程就演变成这样:
SecurityFilterChain0
+----------------------------+
| /api/** |
| +------------------+ |
| | Security Filter0 | |
| +--------+---------+ |
| | |
+----------+ +----------------> +--------+ |
| Client | | | |--------| |
+----+-----+ | | |--------| |
FilterCain | | | |--------| |
+---------------------------------------+ | | |--------| |
| +---------v-----------+ | | | +--------+ |
| | Filter0 | | | | | |
| +---------+-----------+ | | | +--------v---------+ |
| | | | | | Security FilterN | |
| | | | | +------------------+ |
| +--------------v----------------+ | | | |
| | DelegatingFilterProxy | | +----+-----+ +----------------------------+
| | +---------------------------+ | | | |
| | | FilterChainProxy |------------>| select? | +--------------------+
| | +---------------------------+ | | | | | |
| +-------------------------------+ | +----+-----+ +--------------------+
| | | |
| | | | +--------------------+
| +---------v-----------+ | | | |
| | FilterN | | | +--------------------+
| +---------+-----------+ | |
| | | | +--------------------+
| | | | | |
| +---------v-----------+ | | +--------------------+
| | Servlet | | |
| +---------------------+ | | SecurityFilterChainN
+---------------------------------------+ | +----------------------------+
| | /** |
| | |
| | +--------+ |
| | +--------+ |
| | | |
+----------------> +--------+ |
| +--------+ |
| | |
| +--------+ |
| +--------+ |
+----------------------------+
对于多
SecurityFilterChain的配置,只有第一个匹配到的SecurityFilterChain才会被调用。下面展示SecurityFilterChain的匹配规则:
- 如果一个
/api/messages/被请求,它首先匹配到SecurityFilterChain0(/api/**),因此只有SecurityFilterChain0会被调用(即使它也匹配到SecurityFilterChainN)。- 如果一个
/messages/被请求,它则不会匹配SecurityFilterChain0(/api/**),接着FilterChainProxy将会继续尝试下一个SecurityFilterChain,直到匹配成功。假设没有其它SecurityFilterChain可匹配,它则会匹配SecurityFilterChainN并进一步调用。
Bean Filter执行顺序
除此之外,Spring Security还预设了一些默认的Security Filter,其每个Security Filter的执行顺序如下表所示:
| 执行顺序 | 作用 |
|---|---|
ChannelProcessingFilter | 用于确保Web请求通过所需的通道传送,最常见的用法是确保请求在HTTPS上进行。 |
WebAsyncManagerIntegrationFilter | 用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager。 |
SecurityContextPersistenceFilter | 用于在请求前从配置的SecurityContextRepository中填充数据到SecurityContextHolder,并在请求完成和SecurityContextHolder清理后把数据存储回SecurityContextRepository。 |
HeaderWriterFilter | 用于添加headers到当前响应返回以保护浏览者,例如X-Frame-Options、X-XSS-Protection和X-Content-Type-Options。 |
CorsFilter | 用于处理CORS预请求,通过CorsProcessor来拦截CORS请求,并通过CorsConfigurationSource在CORS的响应header中添加相应的Access-Control-Allow-Origin。 |
CsrfFilter | 用于应用CSRF保护。 |
LogoutFilter | 用于处理登录操作。 |
OAuth2AuthorizationRequestRedirectFilter | - |
Saml2WebSsoAuthenticationRequestFilter | - |
X509AuthenticationFilter | - |
AbstractPreAuthenticatedProcessingFilter | 用于处理(预认证)身份验证请求(假设客户端已经经过外部系统认证了),只用作提取关键信息而不做其他处理,当然可以通过修改配置来进行校验。 |
CasAuthenticationFilter | - |
OAuth2LoginAuthenticationFilter | - |
Saml2WebSsoAuthenticationFilter | - |
UsernamePasswordAuthenticationFilter | 用于处理表单提交认证。提交地址和校验参数都存在默认值,可自定义修改。 |
OpenIDAuthenticationFilter | - |
DefaultLoginPageGeneratingFilter | 用于生成默认的登录界面。 |
DefaultLogoutPageGeneratingFilter | 用于生成默认的登出页面。 |
ConcurrentSessionFilter | 用于处理session,比如刷新和过期。 |
DigestAuthenticationFilter | 用于处理http请求的Digest认证,并把结果放到SecurityContextHolder中。另外,此Digest实现已经被设计避免需存储session。 |
BearerTokenAuthenticationFilter | - |
BasicAuthenticationFilter | 用于处理http请求的BASIC认证,并把结果放到SecurityContextHolder中。 |
RequestCacheAwareFilter | 用于负责在缓存匹配当前请求的情况下重组被保存的请求。 |
SecurityContextHolderAwareRequestFilter | 用于将实现了servlet API安全方法的request wrapper填充到ServletRequest。 |
JaasApiIntegrationFilter | 用于获取JAAS并继续以Subject来运行FilterChain。 |
RememberMeAuthenticationFilter | 用于在SecurityContext中没有Authentication对象的情况下,通过在RememberMeServices中获取remember-me、authentication、token并填充上下文。 |
AnonymousAuthenticationFilter | 用于在SecurityContextHolder中没有Authentication对象的情况下通过Anonymous填充。 |
OAuth2AuthorizationCodeGrantFilter | - |
SessionManagementFilter | 用于已认证的用户调用SessionAuthenticationStrategy来执行任何与session有关的活动。 |
ExceptionTranslationFilter | 用于处理在FilterChain中抛出的AccessDeniedException异常和AuthenticationException异常。 |
FilterSecurityInterceptor | 用于执行http资源的安全处理(做权限校验)。 |
SwitchUserFilter | 用于负责用户上下文的切换(切换角色等)。 |
至此,Spring Security架构体系已分析完毕。下面我们再来看看它的两大核心组件:“Authentication身份认证”和“Authorization权限校验”。
Security核心组件
关于Spring Security的“身份认证”和“权限校验”,在FilterChain过滤器链中首先通过身份认证的Security Filter进行判断,并将认证信息存放到SecurityContext中,然后在后面权限校验的Security Filter中从SecurityContext中获取认证信息进行权限校验。
为了更好地理解“Authentication身份认证”和“Authorization权限校验”两大核心组件,下面我们首先来看看存储认证信息的SecurityContext信息模型(上下文模型)。
Security信息模型(SecurityContext)
在Spring Security中,SecurityContext是贯穿“身份验证”和“权限校验”的核心模型。当用户身份认证(一般在AuthenticationManager中处理)成功后会将认证信息Authentication存储到SecurityContext中,在后续的权限校验(一般在AccessDecisionManager中处理)或者业务处理时可以从SecurityContext中取出认证信息Authentication。
对于
Authentication,我们可以将它理解为当前用户的信息/身份。
对于SecurityContext的存储,Spring Security是通过SecurityContextHolder来实现的,而SecurityContextHolder的实现策略默认则是使用线程变量ThreadLocal(保证了SecurityContext的线程安全)。
关于
SecurityContextHolder实现策略,我们可以通过变更系统属性或者方法调用的方式进行修改,即:
- 第
1种方法是通过变更系统属性spring.security.strategy来修改存储策略。- 第
2种方法是通过在使用SecurityContextHolder前调用setStrategyName方法来修改存储策略。其中,
SecurityContextHolder存储策略存在以下几种,即:
存储策略 描述 MODE_THREADLOCAL表示通过 ThreadLocal来存储(默认)。MODE_INHERITABLETHREADLOCAL表示通过 InheritableThreadLocal来存储。MODE_GLOBAL表示通过(全局)静态变量来存储。
这样,整个SecurityContextHolder的层级结构就如下图所示:
+---------------------------------------------------------+
| SecurityContextHolder |
| +-----------------------------------------------------+ |
| | SecurityContext | |
| | +-------------------------------------------------+ | |
| | | Authentication | | |
| | | +-------------+ +-------------+ +-------------+ | | |
| | | | Principal | | Credentials | | Authorities | | | |
| | | +-------------+ +-------------+ +-------------+ | | |
| | +-------------------------------------------------+ | |
| +-----------------------------------------------------+ |
+---------------------------------------------------------+
其中,Authentication主要包含三个属性,分别是:
| 属性 | 作用 |
|---|---|
principal | 用于标识用户身份,一般为ID、手机号、邮箱等。 |
credentials | 用于标识用户凭证,一般是密码。在认证之后需要确保对其进行清理以防泄露。 |
authorities | 用于标识用户所被授予的角色或权限。 |
关于
Authentication的authorities属性,它是通过GrantedAuthority列表来声明的。而GrantedAuthority中仅仅定义了一个方法getAuthority(),getAuthority()方法会返回一个表示当前权限的字符串,这样AccessDecisionManager就能够以最简单的方式对相应的权限进行读取了。/** * Represents an authority granted to an {@link Authentication} object. * * <p> * A <code>GrantedAuthority</code> must either represent itself as a <code>String</code> * or be specifically supported by an {@link AccessDecisionManager}. */ public interface GrantedAuthority extends Serializable { /** * If the <code>GrantedAuthority</code> can be represented as a <code>String</code> * and that <code>String</code> is sufficient in precision to be relied upon for an * access control decision by an {@link AccessDecisionManager} (or delegate), this * method should return such a <code>String</code>. * <p> * If the <code>GrantedAuthority</code> cannot be expressed with sufficient precision * as a <code>String</code>, <code>null</code> should be returned. Returning * <code>null</code> will require an <code>AccessDecisionManager</code> (or delegate) * to specifically support the <code>GrantedAuthority</code> implementation, so * returning <code>null</code> should be avoided unless actually required. * * @return a representation of the granted authority (or <code>null</code> if the * granted authority cannot be expressed as a <code>String</code> with sufficient * precision). */ String getAuthority(); }如果
GrantedAuthority结构复杂而无法精确地用字符串表示,则可以令getAuthority()方法返回null,并在AccessDecisionManager中指定支持这种特殊格式的GrantedAuthority即可。
最后,为了可以更加清晰地展示SecurityContextHolder的作用,笔者大致地将身份认证、权限校验和业务处理的代码贴了出来,即:
身份认证(将认证信息
Authentication存储到SecurityContext)// 简单版身份认证过滤器(忽略异常情况) public class AuthenticationSecurityFilter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 身份认证 Authentication authResult = attemptAuthentication(request, response); // 把认证信息放到线程变量里 SecurityContextHolder.getContext().setAuthentication(authResult); // 继续执行下一个filter/servlet chain.doFilter(request, response); } }权限校验(从
SecurityContext获取认证信息Authentication)// 简单版权限校验过滤器(忽略异常情况) public class AuthorizationSecurityFilter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 从SecurityContextHolder获得Authentication对象 Authentication authenticated = SecurityContextHolder.getContext().getAuthentication(); // 进行权限校验 this.accessDecisionManager.decide(authenticated, ...); // 继续执行下一个filter/servlet chain.doFilter(request, response); } }业务处理(从
SecurityContext获取认证信息Authentication)// 简单版业务处理(忽略异常情况) public class BusinessService { public void execute(){ // 从SecurityContextHolder获得Authentication对象 Authentication authenticated = SecurityContextHolder.getContext().getAuthentication(); // 处理业务 handle(authenticated); } }
至此,我们对SecurityContext模型分析完毕。下面我们再一起来看看“Authentication身份认证”和“Authorization权限校验”两大核心组件。
Security身份认证(Authentication)
对于Authentication身份认证,Spring Secutity提供了很多预设的解决方案,其中最常用的一种就是通过username/password的方式进行认证,它们在实现上主要分为“身份认证”的处理方式和信息存储两个模块,即:
- 对于“身份认证”的处理方式,
Spring Security主要提供了三种类型,分别是Form Login、Basic Authentication和Digest Authentication。 - 对于“身份认证”的信息存储,
Spring Security主要提供了四种类型,分别是In-Memory存储、JDBC存储、自定义UserDetailsService存储和LDAP存储。
关于
username/password认证的处理机制,Spring Security是基于过滤器Filter来实现的,即:
Form Login类型的身份认证是通过UsernamePasswordAuthenticationFilter过滤器实现的,它会提取出表单提交的username/password进行认证处理。Basic Authentication类型的身份认证是通过BasicAuthenticationFilter过滤器实现的,它会提取出请求头中的BASIC认证信息并将其转化为username/password进行认证处理。Digest Authentication类型的身份认证是通过DigestAuthenticationFilter过滤器实现的,它会提取出请求头中的Digest认证信息并计算出密钥串进行认证处理。
根据Spring Security的设计理念,这种类型的身份认证(username/password的方式)主要作用于登陆接口(首次登陆)。但是,如果我们通过这种方式实现登陆,就必然会加大对其开发的难度和压力。因此,笔者建议只通过Spring Security实现接口的认证/权限拦截,而要达到这种效果可以通过3种方式实现,即:
- 通过实现
AbstractAuthenticationProcessingFilter抽象类。 - 通过实现
AbstractPreAuthenticatedProcessingFilter抽象类。 - 通过实现
GenericFilterBean抽象类、OncePerRequestFilter抽象类或者Filter接口。
显然,从
Spring Security实现原理的角度看,通过GenericFilterBean、OncePerRequestFilter或者Filter等基础类来实现认证/权限拦截是最灵活的,但是笔者认为应该在现有能力无法提供的情况下才使用它们来实现,这样才能避免无效的造轮子。同时,也正因如此对于这种情况的实现在这里就不展开探讨了,有兴趣的读者可以自行参考相应的资料。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter作为实现UsernamePasswordAuthenticationFilter的抽象类,它定义了(首次)登陆/授权的执行流程。显然,从这样的定义或设计理念来说,它并不适合用于实现接口的认证/权限拦截功能(不推荐)。
在
AbstractAuthenticationProcessingFilter的实现类中,我们一般只需要实现用于执行身份认证的attemptAuthentication方法即可。例如,在UsernamePasswordAuthenticationFilter的attemptAuthentication方法实现上,它会从请求中获取username和password参数并进行身份认证操作(校验)。
但是,由于笔者当初在搭建Spring Security时工期比较紧张,并没有仔细探讨其中的一些定义或设计理念,因此强行采用了AbstractAuthenticationProcessingFilter来实现接口的认证/权限拦截功能。下面,笔者将展开基于AbstractAuthenticationProcessingFilter实现的认证/权限拦截功能,大家也可以借鉴或者学习一下其中的一些执行流程或者实现原理(如无兴趣,此小节可忽略)。
下面,我们首先来看看AbstractAuthenticationProcessingFilter执行流程图,即:
SecurityFilterChain
+----------------------------+ +----------------+
+----------+ | | | |
| Client | | +------------------+ | | |
+----+-----+ | | Security Filter0 | | | v
FilterCain | | +--------+---------+ | | +-------+--------+
+---------------------------------------+ | | | | | Authentication |
| +---------v-----------+ | | +--------+ | | +-------+--------+
| | Filter0 | | | +--------+ | | |
| +---------+-----------+ | | | | | |
| | | | +----------v-----------+ | | v
| | | | |AbstractAuthentication| | | +-----------+-----------+
| +--------------v----------------+ | | | ProcessingFilter +-----+ | AuthenticationManager |
| | DelegatingFilterProxy | | | +----------+-----------+ | +-----------+-----------+
| | +---------------------------+ | | | | | |
| | | FilterChainProxy +------->+ +--------+ | |
| | +---------------------------+ | | | +--------+ | v
| +-------------------------------+ | | | | +------+-------+
| | | | +--------v---------+ | |Authenticated?|
| | | | | Security FilterN | | +------+-------+
| +---------v-----------+ | | +------------------+ | |
| | FilterN | | | | failure | success
| +---------+-----------+ | +----------------------------+ +----------------+-------------------+
| | | | |
| | | | |
| +---------v-----------+ | +--------------------------------+ +-----------------------------------+
| | Servlet | | | +----------------------------+ | | +-------------------------------+ |
| +---------------------+ | | | SecurityContextHolder | | | | SessionAuthenticationStrategy | |
+---------------------------------------+ | +----------------------------+ | | +-------------------------------+ |
| +----------------------------+ | | +-------------------------------+ |
| | RememberMeServices | | | | SecurityContextHolder | |
| +----------------------------+ | | +-------------------------------+ |
| +----------------------------+ | | +-------------------------------+ |
| |AuthenticationFailureHandler| | | | RememberMeServices | |
| +----------------------------+ | | +-------------------------------+ |
+--------------------------------+ | +-------------------------------+ |
| | ApplicationEventPublisher | |
| +-------------------------------+ |
| +-------------------------------+ |
| | AuthenticationSuccessHandler | |
| +-------------------------------+ |
+-----------------------------------+
- 首先,当用户提交凭据时,
AbstractAuthenticationProcessingFilter过滤器会根据HttpServletRequest创建一个Authentication,其中被创建的Authentication类型依赖于AbstractAuthenticationProcessingFilter的实现(例如,UsernamePasswordAuthenticationFilter将从HttpServletRequest中获取用户提交的username和password来创建一个UsernamePasswordAuthenticationToken)。 - 然后,
Authentication被传递到AuthenticationManager进一步身份认证。- 如果认证失败(一般是抛出
AuthenticationException异常):- 清理
SecurityContextHolder。 - 调用
RememberMeService.loginFail(如果remember me没有被配置则不进行任何操作)。 - 调用
AuthenticationFailureHandler。
- 清理
- 如果认证成功:
- 通知
SessionAuthenticationStrategy有新的用户登录进来。 - 设置
Authentication到SecurityContextHolder中,并在之后的SecurityContextPersistenceFilter中将SecurityContext保存到HttpSession。 - 调用
RememberMeService.loginSuccess(如果remember me没有被配置则不进行任何操作)。 - 通过
ApplicationEventPublishes发布InteractiveAuthenticationSuccessEvent事件。 - 调用
AuthenticationSuccessHandler。
- 通知
- 如果认证失败(一般是抛出
其中,AuthenticationManager是专门用于执行身份认证步骤的(定义)。如果在实现上没有使用到Security Filter,则无需定义AuthenticationManager,对于这种情况直接设置SecurityContextHolder即可。
对于AuthenticationManager,它最常用的实现类就是ProviderManager。ProviderManager在执行身份认证时会委托给AuthenticationProvider列表进行判断,在列表中的每一个AuthenticationProvider都有机会去判断身份认证是成功或是失败(只要适配成功),如果当前AuthenticationProvider无法做出判断(不适配)就会继续流向下一个AuthenticationProvider。但是,如果遍历整个列表都找不到适配的AuthenticationProvider,那么就会抛出ProviderNotFoundException异常(表示不支持当前类型的Authentication)。
关于ProviderManager,它的结构图如下所示:
AuthenticationProviders
+-----------------------------+
| +-------------------------+ |
| | AuthenticationProvider0 | |
| +------------+------------+ |
| | |
+-----------------------+ | +--------+ |
| ProviderManager +---------->+ |--------| |
+-----------------------+ | |--------| |
| +--------+ |
| | |
| +------------v------------+ |
| | AuthenticationProvider0 | |
| +-------------------------+ |
+-----------------------------+
注意,如果存在
AuthenticationProvider适配成功后,无论是认证成功还是认证失败都不会继续适配其他AuthenticationProvider了。
最终,将ProviderManager的实现结构整合AbstractAuthenticationProcessingFilter就演变成这样了:
SecurityFilterChain
+----------------------------+ +----------------+
+----------+ | | | |
| Client | | +------------------+ | | |
+----+-----+ | | Security Filter0 | | | v
FilterCain | | +--------+---------+ | | +-------+--------+ AuthenticationProviders
+---------------------------------------+ | | | | | Authentication | +-----------------------------+
| +---------v-----------+ | | +--------+ | | +-------+--------+ | +-------------------------+ |
| | Filter0 | | | +--------+ | | | | | AuthenticationProvider0 | |
| +---------+-----------+ | | | | | | | +------------+------------+ |
| | | | +----------v-----------+ | | v | | |
| | | | |AbstractAuthentication| | | +-----------+-----------+ | +--------+ |
| +--------------v----------------+ | | | ProcessingFilter +-----+ | ProviderManager +---------->+ |--------| |
| | DelegatingFilterProxy | | | +----------+-----------+ | +-----------+-----------+ | |--------| |
| | +---------------------------+ | | | | | | | +--------+ |
| | | FilterChainProxy +------->+ +--------+ | | | | |
| | +---------------------------+ | | | +--------+ | v | +------------v------------+ |
| +-------------------------------+ | | | | +------+-------+ | | AuthenticationProvider0 | |
| | | | +--------v---------+ | |Authenticated?| | +-------------------------+ |
| | | | | Security FilterN | | +------+-------+ +-----------------------------+
| +---------v-----------+ | | +------------------+ | |
| | FilterN | | | | failure | success
| +---------+-----------+ | +----------------------------+ +----------------+-------------------+
| | | | |
| | | | |
| +---------v-----------+ | +--------------------------------+ +-----------------------------------+
| | Servlet | | | +----------------------------+ | | +-------------------------------+ |
| +---------------------+ | | | SecurityContextHolder | | | | SessionAuthenticationStrategy | |
+---------------------------------------+ | +----------------------------+ | | +-------------------------------+ |
| +----------------------------+ | | +-------------------------------+ |
| | RememberMeServices | | | | SecurityContextHolder | |
| +----------------------------+ | | +-------------------------------+ |
| +----------------------------+ | | +-------------------------------+ |
| |AuthenticationFailureHandler| | | | RememberMeServices | |
| +----------------------------+ | | +-------------------------------+ |
+--------------------------------+ | +-------------------------------+ |
| | ApplicationEventPublisher | |
| +-------------------------------+ |
| +-------------------------------+ |
| | AuthenticationSuccessHandler | |
| +-------------------------------+ |
+-----------------------------------+
另外,
ProviderManager允许继承一个父AuthenticationManager。在这种情况下,如果没有适配到AuthenticationProvider,则可用父AuthenticationManager来进行处理(如果适配的话)。另外对于父AuthenticationManager来说,它可被多个ProviderManager实例所共享,这样我们就可以将一些公共的身份认证逻辑抽离到父AuthenticationProvider中。+-----------------------+ +-----------------------+ | AuthenticationManager | | AuthenticationManager | +----------+------------+ +-----------+-----------+ ^ | |parent parent | parent +-------------------------------+ +------------------+--------------------+ | ProviderManager | | | | +-------------------------+ | +---------------+---------------+ +---------------+---------------+ | | AuthenticationProviders | | | ProviderManager | | ProviderManager | | +-------------------------+ | | +-------------------------+ | | +-------------------------+ | +-------------------------------+ | | AuthenticationProviders | | | | AuthenticationProviders | | | +-------------------------+ | | +-------------------------+ | +-------------------------------+ +-------------------------------+
至此,我们已经将整个AbstractAuthenticationProcessingFilter的认证流程阐述完毕了。但在真正落地过程中可能会遇到一些很奇葩的问题,这是因为Spring Security是基于Java Web前后端不分离的场景而设计的,而当前潮流却是前后端分离的、无Session的服务场景。下面,笔者将从源码的角度来阐述和解决这些问题。
首先,对于前后端分离、无Session的服务来说一般我们想要达到的效果是:当用户授权后,每次发起请求都需要携带凭证token,并在通过AbstractAuthenticationProcessingFilter过滤器时进行解析和校验。然而,由于设计理念的不同,原生的AbstractAuthenticationProcessingFilter它并不是按我们想象的那样进行处理的,所以在使用过程中我们需要对它进行一定程度的改造。
为了更易于理解,我们先来看看原生AbstractAuthenticationProcessingFilter的执行流程:
- 首先通过
requiresAuthentication方法判断当前请求是否可以被当前过滤器处理,如果可以则执行第2步,否定则直接放行到下一个过滤器。 - 然后通过
attemptAuthentication方法(抽象方法)进行身份验证,在其实现类中可以调起AuthenticationManager进来验证(推荐做法)。- 如果
attemptAuthentication方法验证成功,则会继续执行下一个Filter(如有配置放行),最后再执行successfulAuthentication方法(默认会重定向到Spring Security自带的页面上)。 - 如果
attemptAuthentication方法验证失败,则抛出AuthenticationException异常,最后再执行unsuccessfulAuthentication方法(默认会重定向到Spring Security自带的页面上)。
- 如果
相应的源码部分笔者也展示了出来(其中已经把一些无关紧要的代码移除):
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
private AuthenticationSuccessHandler successHandler = ...;
private AuthenticationFailureHandler failureHandler = ...;
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 不符合Filter不处理,放行
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
// 身份认证核心代码处
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
// session处理
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
// 验证失败处理器
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// 验证失败处理器
unsuccessfulAuthentication(request, response, failed);
return;
}
/** 认证成功 **/
// 是否继续放行
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 验证成功处理器
successfulAuthentication(request, response, chain, authResult);
}
// 子类实现具体认证策略
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException;
// 认证失败处理
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
rememberMeServices.loginFail(request, response);
// 此处会实行重定向
failureHandler.onAuthenticationFailure(request, response, failed);
}
// 认证成功处理
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
// 把认证信息放到线程变量里
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// 此处会实行重定向
successHandler.onAuthenticationSuccess(request, response, authResult);
}
}
从上述分析中可得知原生AbstractAuthenticationProcessingFilter执行流程存在的一些问题。那么接下来笔者将根据需求对AbstractAuthenticationProcessingFilter进行一定程度的改造。
首先在身份认证成功之后,我们需要对请求放行。这样做是因为对于每次
Servlet的请求我们都需要进行身份认证,只有认证通过后才能放行。而默认情况下,它在认证成功后就会执行“认证成功处理器”,即页面重定向(导致中断而无法执行Servlet)。对此,我们需要将continueChainBeforeSuccessfulAuthentication变量设置为true,具体可以调用相应的setter方法:public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) { this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication; }然后对认证成功后的
successfulAuthentication方法进行修改。这样做是因为原生AbstractAuthenticationProcessingFilter主要用作(首次)登陆/授权操作,所以它会在认证成功并处理完其他操作后(如继续FilterChain#doFilter方法)才会在successfulAuthentication方法中将认证信息(Authentication对象)保存到SecurityContext中。但是,由于经过改造AbstractAuthenticationProcessingFilter主要用作认证/权限拦截,所以我们需要在继续执行下一个Filter或者Servlet前(通过FilterChain#doFilter方法)将认证信息Authentication对象保存到SecurityContext中,而不是之后。即,此处将successfulAuthentication方法中的Authentication保存操作移除:protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { // 移除Authentication保存操作,因为此时已经执行完往后的所有Filter或者Servlet // SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }同时,在实现上我们需要在
attemptAuthentication认证成功后,FilterChain#doFilter继续执行下一个Filter或者Servlet前将将认证信息Authentication对象保存到SecurityContext中。具体做法可以在attemptAuthentication方法中执行保存操作,或者重写AbstractAuthenticationProcessingFilter的doFilter方法在这之间添加相应的保存代码。最后对默认的
successHandler处理器(认证成功后处理器)和failureHandler处理器(认证失败后处理器)进行覆盖。这样做是因为默认情况下认证成功和失败都会进行页面重定向。即,此处通过setAuthenticationSuccessHandler方法与setAuthenticationFailureHandler方法对默认处理器进行覆盖:public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) { this.successHandler = successHandler; } public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) { this.failureHandler = failureHandler; }另外,我们也可以通过重写
unsuccessfulAuthentication方法和successfulAuthentication方法忽略对successHandler处理器和failureHandler处理器的调用,以避免最终执行页面重定向。但是,本着改动尽量少的原则,笔者不推荐采用这种方式进行处理。
至此,AbstractAuthenticationProcessingFilter过滤器的改造基本完成。最后,我们只需在(实现)子类中实现对应的抽象方法attemptAuthentication即可。
AbstractPreAuthenticatedProcessingFilter
除此之外,Spring Security还提供了一种可以处理从外部系统获取认证的过滤器,即AbstractPreAuthenticatedProcessingFilter。从表面上看或许与我们所需要的目标有所差异,但如果我们将自定义登陆接口独立出来而不进行认证拦截,那么登陆授权就相当于是通过外部系统完成的。这样看来,AbstractPreAuthenticatedProcessingFilter是相当适合我们的目标了。
在写这篇文章时,笔者发现原本使用的
AbstractAuthenticationProcessingFilter并不适合做认证拦截(设计理念不合适),因此便翻阅了大量的Spring Security文档和源码,并在最后找到了最合适的认证拦截过滤器AbstractPreAuthenticatedProcessingFilter。
下面,我们首先来看看AbstractPreAuthenticatedProcessingFilter执行流程图,即:
SecurityFilterChain +----------------------------+
+----------------------------+ | |
+----------+ | | | |
| Client | | +------------------+ | | v
+----+-----+ | | Security Filter0 | | | +-------+--------+
FilterCain | | +--------+---------+ | | | Authentication |
+---------------------------------------+ | | | | +-------+--------+
| +---------v-----------+ | | +--------+ | | |
| | Filter0 | | | +--------+ | | |
| +---------+-----------+ | | | | | v
| | | | +-----------+------------+ | | +-----------+-----------+
| | | | |AbstractPreAuthenticated+-------+ | AuthenticationManager |
| +--------------v----------------+ | | | ProcessingFilter | | +-----------+-----------+
| | DelegatingFilterProxy | | | +-----------+------------+ | |
| | +---------------------------+ | | | | | |
| | | FilterChainProxy +------->+ +--------+ | v
| | +---------------------------+ | | | +--------+ | +------+-------+
| +-------------------------------+ | | | | |Authenticated?|
| | | | +----------v-----------+ | +------+-------+
| | | | |AbstractAuthentication| | |
| +---------v-----------+ | | | ProcessingFilter | | failure | success
| | FilterN | | | +----------+-----------+ | +----------------+-------------------+
| +---------+-----------+ | | | | | |
| | | | +--------+ | | |
| | | | +--------+ | +--------------------------------+ +-----------------------------------+
| +---------v-----------+ | | | | | +----------------------------+ | | +-------------------------------+ |
| | Servlet | | | +--------v---------+ | | | SecurityContextHolder | | | | SecurityContextHolder | |
| +---------------------+ | | | Security FilterN | | | +----------------------------+ | | +-------------------------------+ |
+---------------------------------------+ | +------------------+ | | +----------------------------+ | | +-------------------------------+ |
| | | |AuthenticationFailureHandler| | | | ApplicationEventPublisher | |
+----------------------------+ | +----------------------------+ | | +-------------------------------+ |
+--------------------------------+ | +-------------------------------+ |
| | AuthenticationSuccessHandler | |
| +-------------------------------+ |
+-----------------------------------+
- 首先,当用户提交凭据时,
AbstractPreAuthenticatedProcessingFilter过滤器会根据HttpServletRequest提取出principal和credentials参数并创建一个Authentication(PreAuthenticatedAuthenticationToken类型)。 - 然后,
Authentication被传递到AuthenticationManager进一步身份认证。- 如果认证失败(可忽略抛出的
AuthenticationException异常):- 清理
SecurityContextHolder。 - 调用
AuthenticationFailureHandler。
- 清理
- 如果认证成功:
- 设置
Authentication到SecurityContextHolder中,并在之后的SecurityContextPersistenceFilter中将SecurityContext保存到HttpSession。 - 通过
ApplicationEventPublishes发布InteractiveAuthenticationSuccessEvent事件。 - 调用
AuthenticationSuccessHandler。
- 设置
- 如果认证失败(可忽略抛出的
其中,AuthenticationManager是专门用于执行身份认证步骤的(定义)。如果在实现上没有使用到Security Filter,则无需定义AuthenticationManager,对于这种情况直接设置SecurityContextHolder即可。
对于AuthenticationManager,它最常用的实现类就是ProviderManager。ProviderManager在执行身份认证时会委托给AuthenticationProvider列表进行判断,在列表中的每一个AuthenticationProvider都有机会去判断身份认证是成功或是失败(只要适配成功),如果当前AuthenticationProvider无法做出判断(不适配)就会继续流向下一个AuthenticationProvider。但是,如果遍历整个列表都找不到适配的AuthenticationProvider,那么就会抛出ProviderNotFoundException异常(表示不支持当前类型的Authentication)。
关于ProviderManager,它的结构图如下所示:
AuthenticationProviders
+-----------------------------+
| +-------------------------+ |
| | AuthenticationProvider0 | |
| +------------+------------+ |
| | |
+-----------------------+ | +--------+ |
| ProviderManager +---------->+ |--------| |
+-----------------------+ | |--------| |
| +--------+ |
| | |
| +------------v------------+ |
| | AuthenticationProvider0 | |
| +-------------------------+ |
+-----------------------------+
注意,如果存在
AuthenticationProvider适配成功后,无论是认证成功还是认证失败都不会继续适配其他AuthenticationProvider了。
最终,将ProviderManager的实现结构整合AbstractPreAuthenticatedProcessingFilter就演变成这样了:
SecurityFilterChain +----------------------------+
+----------------------------+ | |
+----------+ | | | |
| Client | | +------------------+ | | v
+----+-----+ | | Security Filter0 | | | +-------+--------+ AuthenticationPro^iders
FilterCain | | +--------+---------+ | | | Authentication | +-----------------------------+
+---------------------------------------+ | | | | +-------+--------+ | +-------------------------+ |
| +---------v-----------+ | | +--------+ | | | | | AuthenticationProvider0 | |
| | Filter0 | | | +--------+ | | | | +------------+------------+ |
| +---------+-----------+ | | | | | v | | |
| | | | +-----------+------------+ | | +-----------+-----------+ | +--------+ |
| | | | |AbstractPreAuthenticated+-------+ | ProviderManager +-------->+ |--------| |
| +--------------v----------------+ | | | ProcessingFilter | | +-----------+-----------+ | |--------| |
| | DelegatingFilterProxy | | | +-----------+------------+ | | | +--------+ |
| | +---------------------------+ | | | | | | | | |
| | | FilterChainProxy +------->+ +--------+ | v | +------------v------------+ |
| | +---------------------------+ | | | +--------+ | +------+-------+ | | AuthenticationProvider0 | |
| +-------------------------------+ | | | | |Authenticated?| | +-------------------------+ |
| | | | +----------v-----------+ | +------+-------+ +-----------------------------+
| | | | |AbstractAuthentication| | |
| +---------v-----------+ | | | ProcessingFilter | | failure | success
| | FilterN | | | +----------+-----------+ | +----------------+-------------------+
| +---------+-----------+ | | | | | |
| | | | +--------+ | | |
| | | | +--------+ | +--------------------------------+ +-----------------------------------+
| +---------v-----------+ | | | | | +----------------------------+ | | +-------------------------------+ |
| | Servlet | | | +--------v---------+ | | | SecurityContextHolder | | | | SecurityContextHolder | |
| +---------------------+ | | | Security FilterN | | | +----------------------------+ | | +-------------------------------+ |
+---------------------------------------+ | +------------------+ | | +----------------------------+ | | +-------------------------------+ |
| | | |AuthenticationFailureHandler| | | | ApplicationEventPublisher | |
+----------------------------+ | +----------------------------+ | | +-------------------------------+ |
+--------------------------------+ | +-------------------------------+ |
| | AuthenticationSuccessHandler | |
| +-------------------------------+ |
+-----------------------------------+
另外,
ProviderManager允许继承一个父AuthenticationManager。在这种情况下,如果没有适配到AuthenticationProvider,则可用父AuthenticationManager来进行处理(如果适配的话)。另外对于父AuthenticationManager来说,它可被多个ProviderManager实例所共享,这样我们就可以将一些公共的身份认证逻辑抽离到父AuthenticationProvider中。+-----------------------+ +-----------------------+ | AuthenticationManager | | AuthenticationManager | +----------+------------+ +-----------+-----------+ ^ | |parent parent | parent +-------------------------------+ +------------------+--------------------+ | ProviderManager | | | | +-------------------------+ | +---------------+---------------+ +---------------+---------------+ | | AuthenticationProviders | | | ProviderManager | | ProviderManager | | +-------------------------+ | | +-------------------------+ | | +-------------------------+ | +-------------------------------+ | | AuthenticationProviders | | | | AuthenticationProviders | | | +-------------------------+ | | +-------------------------+ | +-------------------------------+ +-------------------------------+
从AbstractPreAuthenticatedProcessingFilter执行流程来看,或许感觉它与AbstractAuthenticationProcessingFilter差不多,但实际AbstractPreAuthenticatedProcessingFilter在源码层面上还是下了一些功夫的。比如说,默认情况下AbstractPreAuthenticatedProcessingFilter在处理时仅仅会从请求中提取出必要的信息,而不会像AbstractAuthenticationProcessingFilter那样对它们进行身份认证(校验),具体如下所示:
public abstract class AbstractPreAuthenticatedProcessingFilter extends ... {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 判断是否执行身份信息的提取或者校验
if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
// 执行身份信息的提取或者校验
doAuthenticate((HttpServletRequest) request, (HttpServletResponse) response);
}
chain.doFilter(request, response);
}
private void doAuthenticate(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
Authentication authResult;
// 获取principal
Object principal = getPreAuthenticatedPrincipal(request);
// 获取credentials
Object credentials = getPreAuthenticatedCredentials(request);
if (principal == null) {
return;
}
try {
// 根据principal和credentials生成Authentication
PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(principal, credentials);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
// 执行身份认证(校验)
authResult = authenticationManager.authenticate(authRequest);
// 成功处理器
successfulAuthentication(request, response, authResult);
}
catch (AuthenticationException failed) {
// 失败处理器
unsuccessfulAuthentication(request, response, failed);
// 判断是否忽略认证失败
if (!continueFilterChainOnUnsuccessfulAuthentication) {
throw failed;
}
}
}
}
通过AbstractPreAuthenticatedProcessingFilter源码的阅读,我们可以发现实际上AbstractPreAuthenticatedProcessingFilter也是会进行身份认证的,在认证失败时它也会抛出AuthenticationException异常,只不过在对AuthenticationException异常的处理时通过一个continueFilterChainOnUnsuccessfulAuthentication标识来判断是否会异常进行忽略(默认是忽略的)。
至此,我们已经将整个AbstractPreAuthenticatedProcessingFilter的认证流程阐述完毕了。而对于它的使用,我们只需要在AbstractPreAuthenticatedProcessingFilter实现类中实现getPreAuthenticatedPrincipal方法和getPreAuthenticatedCredentials方法,并将它和它对应的认证处理器ProviderManager(能够处理PreAuthenticatedAuthenticationToken类型的Authentication)添加到Spring Security配置中即可。
Security权限校验(Authorization)
在通过了身份认证的过滤器后(例如,AbstractAuthenticationProcessingFilter),马上就是进一步的权限校验,即Authorization。在实现上,对于权限校验的处理主要涉及了AbstractSecurityInterceptor过滤器。这样,整个Spring Security的执行流程就演变成这样:
SecurityFilterChain
+----------------------------+
| |
| +------------------+ |
| | Security Filter0 | | +------------------------+
| +--------+---------+ | | |
+----------+ | | | | |
| Client | | +--------+ | | +-------v--------+ +-----------------------+
+----+-----+ | +--------+ | | | Authentication +<------+ SecurityContextHolder |
FilterCain | | | | | +-------+--------+ +-----------------------+
+---------------------------------------+ | v | | |
| +---------v-----------+ | | +-----------+------------+ | | +-------v--------+
| | Filter0 | | | |AbstractPreAuthenticated| | | |FilterInvocation|
| +---------+-----------+ | | | ProcessingFilter | | | +-------+--------+
| | | | +-----------+------------+ | | |
| | | | | | | +-------v--------+ +-----------------------+
| +--------------v----------------+ | | +--------+ | | |ConfigAttributes+<------+SecurityMetadataSource |
| | DelegatingFilterProxy | | | +--------+ | | +-------+--------+ +-----------------------+
| | +---------------------------+ | | | | | | |
| | | FilterChainProxy +------->+ +----------v-----------+ | | +-----------v-----------+
| | +---------------------------+ | | | |AbstractAuthentication| | | | AccessDecisionManager |
| +-------------------------------+ | | | ProcessingFilter | | | +-----------+-----------+
| | | | +----------+-----------+ | | |
| | | | | | | +------v-------+
| +---------v-----------+ | | +--------+ | | | Authorized? |
| | FilterN | | | +--------+ | | +------+-------+
| +---------+-----------+ | | | | | |
| | | | +----------v-----------+ | | Denied v success
| | | | | FilterSecurity +-----+ +-----------+-------------+
| +---------v-----------+ | | | Interceptor | | | |
| | Servlet | | | +----------+-----------+ | v v
| +---------------------+ | | | | +-------------------------+ +----------+----------+
+---------------------------------------+ | | | |-------------------------| | Continue Processing |
| +--------v---------+ | || AccessDeniedException || | Request Normally |
| | Security FilterN | | |-------------------------| | |
| +------------------+ | +-------------------------+ +---------------------+
| |
+----------------------------+
其中,FilterSecurityInterceptor就是以Filter的方式来实现权限校验的AbstractSecurityInterceptor实现类(专用于HttpServletRequest的处理),具体的执行流程如下所示:
- 首先,
FilterSecurityInterceptor从SecurityContextHolder中获取Authentication。 - 然后,
FilterSecurityInterceptor根据传入的HttpServletRequest、HttpServletResponse和FilterChain创建一个FilterInvocation。 - 接着,通过把
FilterInvocation传入SecurityMetadataSource来获取ConfigAttribute。 - 最后,将
Authentication、FilterInvocation和ConfigAttribute三个参数传入AccessDecisionManager,并在AccessDecisionManager中进行权限校验:- 如果授权被拒绝(权限校验失败),则抛出
AccessDeniedException异常(此异常将会被传递到ExceptionTranslationFilter中进行处理)。 - 如果授权被同意(权限校验成功),
FilterSecurityInterceptor将继续执行FilterChain的下一个过滤器或者Servlet。
- 如果授权被拒绝(权限校验失败),则抛出
在FilterSecurityInterceptor的作用下,我们就可以通过类似于过滤器Filter配置的方式对权限校验了,例如:
// demo1
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
);
}
// demo2
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.mvcMatchers("/resources/**", "/signup", "/about").permitAll()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().denyAll()
);
}
// demo3
public class WebSecurity {
public boolean check(Authentication authentication, HttpServletRequest request) {
...
}
public boolean checkUserId(Authentication authentication, int id) {
...
}
}
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
...
)
}
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
...
);
}
除此之外,Spring Security为了能够更加方便地进行权限校验,其引入了权限的注解式配置(类注解/方法注解)。在实现上,它是通过MethodSecurityInterceptor(AbstractSecurityInterceptor的实现类)以动态代理的方式来实现的(不是基于过滤器Filter实现的)。这样,整个Spring Security的执行流程就演变成这样:
SecurityFilterChain
+----------------------------+
| |
| +------------------+ |
| | Security Filter0 | |
| +--------+---------+ |
| | |
| +--------+ |
| +--------+ |
| | |
| v |
| +-----------+------------+ |
| |AbstractPreAuthenticated| |
| | ProcessingFilter | |
| +-----------+------------+ |
+----------+ | | |
| Client | | +--------+ |
+----+-----+ | +--------+ |
FilterCain | | | |
+---------------------------------------+ | v |
| | | | +----------+-----------+ |
| | | | |AbstractAuthentication| |
| +---------v-----------+ | | | ProcessingFilter | |
| | Filter0 | | | +----------+-----------+ |
| +---------+-----------+ | | | | +-----------------------+
| | | | +--------+ | | SecurityContextHolder |
| | | | +--------+ | +----------+------------+
| | | | | | |
| +--------------v----------------+ | | +----------v-----------+ | +-------v--------+ +----------------+
| | DelegatingFilterProxy | | | | FilterSecurity +-------->+ Authentication +------>+FilterInvocation|
| | +---------------------------+ | | | | Interceptor | | +----------------+ +-------+--------+ +----------------------------------------------------------------------+
| | | FilterChainProxy +------->+ +----------+-----------+ | | | |
| | +---------------------------+ | | | | | | | +-----------------------+ |
| +-------------------------------+ | | v | | | |SecurityMetadataSource | AbstractSecurityInterceptor |
| | | | +--------+---------+ | | | +-----------+-----------+ |
| | | | | Security FilterN | | | | | |
| | | | +------------------+ | | | +-------v--------+ +-----------------------+ |
| +---------v-----------+ | | | +--------------->+ConfigAttributes+---->+ AccessDecisionManager | |
| | FilterN | | +----------------------------+ | | +----------------+ +-----------+-----------+ |
| +---------+-----------+ | | | | |
| | | Spring IoC Container | | +------v-------+ |
| | | +----------------------------+ | | | Authorized? | |
| | | | MethodSecurityInterceptor | | | +------+-------+ |
| +---------v-----------+ | | +---------------------+ | | | | |
| | Servlet +---------->+ | Proxy(AOP) | | +----------------+ +-------+--------+ | Denied v success |
| +---------------------+ | | | +---------------+ +--------->+ Authentication +------>+MethodInvocation| | +-----------+-------------+ |
| | | | | plain object | | | +-------+--------+ +----------------+ | | | |
| | | | +---------------+ | | ^ | v v |
+---------------------------------------+ | +---------------------+ | +----------+------------+ | +-------------------------+ +----------+----------+ |
+----------------------------+ | SecurityContextHolder | | |-------------------------| | Continue Processing | |
+-----------------------+ | || AccessDeniedException || | Request Normally | |
| |-------------------------| | | |
| +-------------------------+ +---------------------+ |
| |
+----------------------------------------------------------------------+
其中,MethodSecurityInterceptor的执行流程与FilterSecurityInterceptor类似,具体如下所示:
- 首先,
MethodSecurityInterceptor从SecurityContextHolder中获取Authentication。 - 然后,通过动态代理获取
MethodInvocation并将其传入SecurityMetadataSource来获取ConfigAttribute。 - 最后,将
Authentication、MethodInvocation和ConfigAttribute三个参数传入到AccessDecisionManager,并在AccessDecisionManager中进行权限校验:- 如果授权被拒绝(权限校验失败),则抛出
AccessDeniedException异常(此异常将会被传递到ExceptionTranslationFilter中进行处理)。 - 如果授权被同意(权限校验成功),
MethodSecurityInterceptor将继续执行拦截链中的下一个拦截器或者被代理的方法。
- 如果授权被拒绝(权限校验失败),则抛出
当然,
Spring Security也提供了AspectJMethodSecurityInterceptor(AbstractSecurityInterceptor的实现类)以AspectJ编译器的方式实现注解式配置,有兴趣的读者可以自行翻阅相关资料。
与基于FilterSecurityInterceptor实现的过滤器式配置不同,使用注解式配置需要添加“@EnableGlobalMethodSecurity”注解,并在其中设置prePostEnabled属性和securedEnabled属性来启用相应的注解功能,即:
/**
* 设置属性prePostEnabled=true,即启动注解@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter
* 设置属性securedEnabled=true,即启动注解@Secured
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {}
| 注解 | 说明 |
|---|---|
@PreAuthorize | 专用于方法访问前进行校验处理。 |
@PostAuthorize | 专用于方法返回前进行校验处理。 |
@PreFilter | 专用于方法访问前对数据进行校验处理,它会将校验失败的请求数据移除(迭代)。 |
@PostFilter | 专用于方法返回前对数据进行校验处理,它会将校验失败的返回数据移除(迭代)。 |
@Secured | 专用于方法访问前进行校验处理。 |
具体使用方式如下所示:
@PreAuthorize("hasRole('USER')")
public void create(Contact contact);
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);
/**
* 通过注解@P可将别名用于@PreAuthorize
*/
@PreAuthorize("#c.name == authentication.name")
public void doSomething(@P("c") Contact contact);
/**
* 通过注解@Param可将别名用于@PreAuthorize
*/
@PreAuthorize("#n == authentication.name")
Contact findContactByName(@Param("n") String name);
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
关于权限校验,我们可以使用如下表达式进行配置:
表达式 作用 hasRole(String role)如果当前用户存在指定角色则返回 true。需要注意,它默认会追加ROLE_前缀到role上(如果不存在ROLE_前缀),可通过修改DefaultWebSecurityExpressionHandler中的defaultRolePrefix来修改其前缀。hasAnyRole(String... roles)如果当前用户存在指定的任意一个角色则返回 true。需要注意,它默认会追加ROLE_前缀到role上(如果不存在ROLE_前缀),可通过修改DefaultWebSecurityExpressionHandler中的defaultRolePrefix来修改其前缀。hasAuthority(String authority)如果当前用户存在指定的权限则返回 true。hasAnyAuthority(String... authorities)如果当前用户存在指定的任意一个权限则返回 true。principal表示当前用户,可通过 principal来访问当前用户。authentication表示从 SecurityContext获得的Authentication对象,可通过authentication来访问当前Authentication对象。permitAll表示允许所有用户访问,总是返回 true。denyAll表示拒绝所有用户访问,总是返回 false。isAnonymous()如果当前用户是一个 anonymous匿名用户则返回true。isRememberMe()如果当前用户是一个 remember-me用户则返回true。isAuthenticated()如果当前用户不是一个 anonymous匿名用户则返回true。isFullyAuthenticated()如果当前用户不是一个 anonymous匿名用户或remember-me用户,则返回true。hasPermission(Object target, Object permission)如果当前用户对“指定目标”具有“指定权限”则返回 true。hasPermission(Object targetId, String targetType, Object permission)如果当前用户对“指定目标”存在“指定权限”则返回 true。
至此,我们对Spring Security权限校验的执行流程和使用方式都阐述完毕。下面,我们再详细来看看它是如何实现权限校验的:
对于权限校验的处理,无论是FilterSecurityInterceptor还是MethodSecurityInterceptor,它们都是交由AbstractSecurityInterceptor进行处理的,即:
// 为了便于理解,笔者已将部分非核心代码移除
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
// 为了便于理解,笔者已将部分非核心代码移除
public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation mi) throws Throwable {
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
result = mi.proceed();
}
finally {
super.finallyInvocation(token);
}
return super.afterInvocation(token, result);
}
}
本质上,它们所执行的方法和步骤都是相同的,即:
- 首先执行
beforeInvocation方法进行权限校验,并返回InterceptorStatusToken(临时保存安全校验时所获取/计算的信息)。 - 然后执行“过滤链/拦截链"中的下一个处理器(可能是过滤器、拦截器、
Servlet、被代理方法等)。 - 接着执行
finallyInvocation方法进行信息清理工作。 - 最后执行
afterInvocation方法进行后置处理工作。
此处,重点关注的是beforeInvocation方法的权限校验,具体校验逻辑如下所示:
// 为了便于理解,笔者已将部分非核心代码移除
public abstract class AbstractSecurityInterceptor implements InitializingBean, ... {
protected InterceptorStatusToken beforeInvocation(Object object) {
// 从SecurityMetadataSource中获得关联的ConfigAttribute
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 从SecurityContextHolder获得Authentication对象(如有必要会进一步执行身份认证)
Authentication authenticated = authenticateIfRequired();
try {
// 进行权限校验
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
// ...
throw accessDeniedException;
}
// 构建运行时Authentication对象
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
// 运行时Authentication对象为空,则忽略
if (runAs == null) {
// 第2个参数false表示无需刷新SecurityContextHolder
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
else {
SecurityContext origCtx = SecurityContextHolder.getContext();
// 运行时Authentication对象不为空,则替换
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// 将原Authentication对象返回,用于后面进行恢复处理。其中,第2个参数true表示需要刷新SecurityContextHolder
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
/**
* Checks the current authentication token and passes it to the AuthenticationManager
* if {@link org.springframework.security.core.Authentication#isAuthenticated()}
* returns false or the property <tt>alwaysReauthenticate</tt> has been set to true.
*
* @return an authenticated <tt>Authentication</tt> object.
*/
private Authentication authenticateIfRequired() {
// 从SecurityContextHolder获得Authentication对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 如果身份已经认证(授权)并且无需(总是)重复授权,则返回Authentication对象
if (authentication.isAuthenticated() && !alwaysReauthenticate) {
return authentication;
}
// 执行身份认证(授权),如果认证失败会抛出AuthenticationException异常
authentication = authenticationManager.authenticate(authentication);
// 将Authentication对象保存到SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
/**
* AbstractSecurityInterceptor中通过InitializingBean的方法afterPropertiesSet()在启动时对当前配置项进行检查(判断是否符合条件),例如,当前AccessDecisionManager是否适用于当前调用类等。
*
* 通过这种方式就能让我们在启动时提前感知到所存在的配置问题,同时也防止了在实现类中对某些配置的遗漏,提前将问题暴露。
*
* 这部分代码笔者并没有放出来,有兴趣的同学可以自行阅读源码
*/
public void afterPropertiesSet() {
//... 对AbstractSecurityInterceptor配置进行校验
}
}
为了能更清晰地了解权限校验流程,笔者将具体的执行逻辑整理了下来,具体如下所示:
- 通过
SecurityMetadataSource获得所关联的ConfigAttribute。 - 通过
SecurityContextHolder获得Authentication对象。- 如果
Authentication#isAuthenticated()返回false(尚未身份认证),则通过AuthenticationManager执行身份认证,并将返回的Authentication对象保存到SecurityContextHolder中(如认证成功)。 - 如果
alwaysReauthenticate为true(总是重复身份认证),则通过AuthenticationManager执行身份认证,并将返回的Authentication对象保存到SecurityContextHolder中(如认证成功)。
- 如果
- 通过
AccessDecisionManager执行权限校验。- 如果权限校验失败,则会抛出
AccessDeniedException异常。
- 如果权限校验失败,则会抛出
- 通过
RunAsManager构建运行时Authentication对象。- 如果存在,则将
SecurityContextHolder中保存的Authentication对象替换为运行时Authentication对象。 - 如果不存在,则忽略构建的运行时
Authentication对象。
- 如果存在,则将
- 通过构建
InterceptorStatusToken将权限校验时所获取/计算的信息返回。
关于
RunAsManager,它主要用于为当前调用的安全对象创建一个临时的Authentication对象,并通过这种机制来提高系统的安全性。基于RunAsManager的这种设计理念,我们可以创建一个具有两层对象的系统,其中一层为面向公共的,可被外部调用者所持有;另一层则是私有的,它仅能被面向公共层里的对象所调用。对此,我们可以简单这样理解:系统业务代码为外部调用者,所使用的对象即为面向公共的对象;Spring Security框架本身为面向公共的对象,所使用的对象包含私有对象。在
AbstractSecurityInterceptor我们可以看到在beforeInvocation方法中会将SecurityContextHolder中保存的Authentication对象替换为运行时Authentication对象(如存在),即:// 构建运行时Authentication对象 Authentication runAs = this.runAsManager.buildRunAs(authenticated,object, attributes); /** 此处忽略runAs不存在的情况 **/ SecurityContext origCtx = SecurityContextHolder.getContext(); // 运行时Authentication对象不为空,则替换 SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContex()); SecurityContextHolder.getContext().setAuthentication(runAs); // 将原Authentication对象返回,用于后面进行恢复处理 return new InterceptorStatusToken(origCtx, true, attributes, object);这样,在“过滤链/拦截链"中的下一个处理器执行时从
SecurityContextHolder获取的Authentication对象即为RunAsManager生成的临时Authentication对象(可被外部调用者所持有的公共对象)。最后,在“过滤链/拦截链"处理完成后执行
finallyInvocation方法或者afterInvocation方法将SecurityContextHolder中的Authentication对象恢复为原Authentication对象(只能被公共对象调用的私有对象),即:protected void finallyInvocation(InterceptorStatusToken token) { // 在将SecurityContextHolder中的Authentication对象替换为RunAsManager所生成的临时Authentication对象时,会将contextHolderRefreshRequired变量设置为true if (token != null && token.isContextHolderRefreshRequired()) { if (logger.isDebugEnabled()) { logger.debug("Reverting to original Authentication: " + token.getSecurityContext().getAuthentication()); } // 将SecurityContextHolder中的Authentication对象恢复为原Authentication对象 SecurityContextHolder.setContext(token.getSecurityContext()); } } protected Object afterInvocation(InterceptorStatusToken token, Object > returnedObject) { // ... finallyInvocation(token); // continue to clean in this method for passivity // ... }通过这种方式,我们可以保证在业务代码中对
Authentication对象的修改并不会影响到Spring Security框架本身对Authentication对象的使用,从而提高系统的安全性。
从上述流程可以看到,其中最核心的权限校验是委托给了AccessDecisionManager进行处理的,而关于AccessDecisionManager源码及其接口方法的作用如下所示:
/**
* Makes a final access control (authorization) decision.
*
* @author Ben Alex
*/
public interface AccessDecisionManager {
/**
* Resolves an access control decision for the passed parameters.
*
* @param authentication the caller invoking the method (not null)
* @param object the secured object being called
* @param configAttributes the configuration attributes associated with the secured
* object being invoked
*
* @throws AccessDeniedException if access is denied as the authentication does not
* hold a required authority or ACL privilege
* @throws InsufficientAuthenticationException if access is denied as the
* authentication does not provide a sufficient level of trust
*/
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException;
/**
* Indicates whether this <code>AccessDecisionManager</code> is able to process
* authorization requests presented with the passed <code>ConfigAttribute</code>.
* <p>
* This allows the <code>AbstractSecurityInterceptor</code> to check every
* configuration attribute can be consumed by the configured
* <code>AccessDecisionManager</code> and/or <code>RunAsManager</code> and/or
* <code>AfterInvocationManager</code>.
* </p>
*
* @param attribute a configuration attribute that has been configured against the
* <code>AbstractSecurityInterceptor</code>
*
* @return true if this <code>AccessDecisionManager</code> can support the passed
* configuration attribute
*/
boolean supports(ConfigAttribute attribute);
/**
* Indicates whether the <code>AccessDecisionManager</code> implementation is able to
* provide access control decisions for the indicated secured object type.
*
* @param clazz the class that is being queried
*
* @return <code>true</code> if the implementation can process the indicated class
*/
boolean supports(Class<?> clazz);
}
| 方法 | 作用 |
|---|---|
decide(...) | decide(...)方法会通过传入的相关信息作出授权的决定(即,权限校验),如果授权失败则抛出AccessDeniedException异常。 |
support(ConfigAttribute) | support(ConfigAttribute)方法会判断AccessDecisionManager是否能处理传入的ConfigAttribute。 |
support(Class) | support(Class)方法会判断AccessDecisionManager是否支持传入的安全对象类型。 |
对于AccessDecisionManager的实现,Spring Security预设了3种不同策略,分别为:
| 策略 | 效果 |
|---|---|
ConsensusBased | 少数服从多数,其中提供了一个属性让我们决定是否允许同意与拒绝的票数相等或全部投中立。 |
AffirmativeBased | 只要存在一个赞同就同意授权,其中提供了一个属性让我们决定是否允许全部投中立。 |
UnanimousBased | 只要存在一个驳回就拒绝授权,其中提供了一个属性让我们决定是否允许全部投中立。 |
如果这三种策略都不符合,可通过继承
AccessDecisionManager来实现自定义的策略。
而Spring Security预设的3种实现实际上所采取的是一种投票策略,在实现上Spring Security将这些投票策略里的投票者抽象为AccessDecisionVoter,即:
/**
* Indicates a class is responsible for voting on authorization decisions.
* <p>
* The coordination of voting (ie polling {@code AccessDecisionVoter}s, tallying their
* responses, and making the final authorization decision) is performed by an
* {@link org.springframework.security.access.AccessDecisionManager}.
*
* @author Ben Alex
*/
public interface AccessDecisionVoter<S> {
// ~ Static fields/initializers
// =====================================================================================
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
// ~ Methods
// ========================================================================================================
/**
* Indicates whether this {@code AccessDecisionVoter} is able to vote on the passed
* {@code ConfigAttribute}.
* <p>
* This allows the {@code AbstractSecurityInterceptor} to check every configuration
* attribute can be consumed by the configured {@code AccessDecisionManager} and/or
* {@code RunAsManager} and/or {@code AfterInvocationManager}.
*
* @param attribute a configuration attribute that has been configured against the
* {@code AbstractSecurityInterceptor}
*
* @return true if this {@code AccessDecisionVoter} can support the passed
* configuration attribute
*/
boolean supports(ConfigAttribute attribute);
/**
* Indicates whether the {@code AccessDecisionVoter} implementation is able to provide
* access control votes for the indicated secured object type.
*
* @param clazz the class that is being queried
*
* @return true if the implementation can process the indicated class
*/
boolean supports(Class<?> clazz);
/**
* Indicates whether or not access is granted.
* <p>
* The decision must be affirmative ({@code ACCESS_GRANTED}), negative (
* {@code ACCESS_DENIED}) or the {@code AccessDecisionVoter} can abstain (
* {@code ACCESS_ABSTAIN}) from voting. Under no circumstances should implementing
* classes return any other value. If a weighting of results is desired, this should
* be handled in a custom
* {@link org.springframework.security.access.AccessDecisionManager} instead.
* <p>
* Unless an {@code AccessDecisionVoter} is specifically intended to vote on an access
* control decision due to a passed method invocation or configuration attribute
* parameter, it must return {@code ACCESS_ABSTAIN}. This prevents the coordinating
* {@code AccessDecisionManager} from counting votes from those
* {@code AccessDecisionVoter}s without a legitimate interest in the access control
* decision.
* <p>
* Whilst the secured object (such as a {@code MethodInvocation}) is passed as a
* parameter to maximise flexibility in making access control decisions, implementing
* classes should not modify it or cause the represented invocation to take place (for
* example, by calling {@code MethodInvocation.proceed()}).
*
* @param authentication the caller making the invocation
* @param object the secured object being invoked
* @param attributes the configuration attributes associated with the secured object
*
* @return either {@link #ACCESS_GRANTED}, {@link #ACCESS_ABSTAIN} or
* {@link #ACCESS_DENIED}
*/
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
其中,对于每个投票者都有一个投票方法vote,它的返回值表示投票结果。在AccessDecisionVoter中规定了3种可选的结果,即:
| 可选值 | 说明 |
|---|---|
ACCESS_GRANTED = 1 | 在授权决策中持有同意态度。 |
ACCESS_ABSTAIN = 0 | 在授权决策中持有中立态度。 |
ACCESS_DENIED = -1 | 在授权决策中持有拒绝态度。 |
关于
AccessDecisionVoter的两个不同类型的supports方法与AccessDecisionManager中的类似,即分别从传入的ConfigAttribute和安全对象类型判断当前投票者(Voter)是否可以进行投票。
同样的,Spring Security也提供了几种不同类型的投票者实现(继承自AccessDecisionVoter),具体如下所示:
| 投票者 | 作用 |
|---|---|
RoleVoter | 专用于作角色判断的投票者,默认会对以ROLE_开头的字符串(通过getAuthority()返回)进行判断。 |
RoleHierarchyVoter | 专用于作(具有层次结构的)角色判断的投票者。在基于RoleVoter的作用上新增了“包含”的逻辑关系,即当设置ROLE_ADMIN > ROLE_STAFF时,ROLE_ADMIN包含了ROLE_STAFF的权限。 |
AuthenticatedVoter | 专用于anonymous、fully-authenticated和remember-me三种用户判断的投票者,可选值有IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY(分别表示用户的不同认证状态)。 |
Jsr250Voter | 专用于Jsr250风格的权限判断的投票者。 |
PreInvocationAuthorizationAdviceVoter | 专用于@PreFilter和@PreAuthorize注解所标识的表达式判断的投票者。 |
WebExpressionVoter | 专用于Web表达式校验的投票者。 |
如果以上投票者都不适合业务的需求,就需要通过继承
AccessDecisionVoter来实现自定义的投票者了。
至此,对Spring Security的权限校验模块分析完毕。
Security异常处理
另外,对于Spring Security身份认证失败所抛出的AuthenticationException异常和权限校验失败所抛出的AccessDeniedException异常则是通过异常过滤器ExceptionTranslationFilter进行处理的。这样,整个Spring Security的执行流程就演变成这样:
SecurityFilterChain
+----------------------------+
| |
| +------------------+ |
| | Security Filter0 | |
+----------+ | +--------+---------+ |
| Client | | | |
+----+-----+ | +--------+ |
FilterCain | | +--------+ |
+---------------------------------------+ | | |
| +---------v-----------+ | | v |
| | Filter0 | | | +-----------+------------+ |
| +---------+-----------+ | | |AbstractPreAuthenticated| |
| | | | | ProcessingFilter | |
| | | | +-----------+------------+ |
| +--------------v----------------+ | | | | +-----------------------+
| | DelegatingFilterProxy | | | +--------+ | | |
| | +---------------------------+ | | | +--------+ | | |
| | | FilterChainProxy +------->+ | | | |
| | +---------------------------+ | | | +----------v-----------+ | | +---------+---------+
| +-------------------------------+ | | |AbstractAuthentication| | | |Continue Processing|
| | | | | ProcessingFilter | | | | Request Normally |
| | | | +----------+-----------+ | | +---------+---------+
| +---------v-----------+ | | | | | |
| | FilterN | | | +--------+ | | |
| +---------+-----------+ | | +--------+ | | +
| | | | | | | Security Exception Judgment
| | | | +----------+-----------+ | | +---------------------------------+
| +---------v-----------+ | | | ExceptionTranslation +---------+ | |
| | Servlet | | | | Filter | | v |
| +---------------------+ | | +----------+-----------+ | Start Authentication Access Denied v
+---------------------------------------+ | | | +----------------------------+ +-------------------------+
| +----------+-----------+ | | +------------------------+ | | +---------------------+ |
| | FilterSecurity | | | | SecurityContextHolder | | | | AccessDeniedHandler | |
| | Interceptor | | | +------------------------+ | | +---------------------+ |
| +----------+-----------+ | | +------------------------+ | +-------------------------+
| | | | | RequestCache | |
| +--------v---------+ | | +------------------------+ |
| | Security FilterN | | | +------------------------+ |
| +------------------+ | | |AuthenticationEntryPoint| |
| | | +------------------------+ |
+----------------------------+ +----------------------------+
对于ExceptionTranslationFilter,它还是通过Filter的方式来实现异常的处理,具体的执行流程如下所示:
- 首先,在
ExceptionTranslationFilter中调用FilterChain.doFilter(request, response)方法继续执行下一个Filter或Servlet。 - 如果在后续的
Filter或Servlet中由于身份认证失败或者其他原因而导致抛出AuthenticationException异常,则启动身份认证流程。- 清理
SecurityContextHolder。 - 将
HttpServletRequest保存到RequestCache中,并在用户身份认证成功后将其(RequestCache)重放到请求中。 - 通过
AuthenticationEntryPoint通知客户端执行身份认证(请求凭证)。
- 清理
- 如果在后续的
Filter或Servlet中由于权限校验失败或者其他原因而导致抛出AccessDeniedException异常,则通过AccessDeniedHandler执行拒绝访问处理。
其中在AuthenticationEntryPoint和AccessDeniedHandler的处理中,我们可以执行重定向到相应的页面或者抛出异常等,而在前后端分离的场景下一般可通过返回特定错误码让前端跳转到登录/授权页面进一步操作。
需要注意,一般在
SpringBoot应用中都会声明一个@RestControllerAdvice HandlerExceptionResolver异常处理器,这种处理器会在ExceptionTranslationFilter过滤器前就对异常进行提前拦截处理了。如果在HandlerExceptionResolver中对异常进行了兜底处理,比如捕获Exception异常等,那么ExceptionTranslationFilter将不生效。对于这种情况可在HandlerExceptionResolver中添加对AccessDeniedException和AuthenticationException异常的支持,即:@RestControllerAdvice public class JwtExceptionHandler { @ResponseStatus(HttpStatus.OK) @ExceptionHandler(AccessDeniedException.class) public Response<Void> handleAccessDeniedException(AccessDeniedException ex) { return Response.fail(AUTH_DENY, "权限不足"); } @ResponseStatus(HttpStatus.OK) @ExceptionHandler(AuthenticationException.class) public Response<Void> handleAccessDeniedException(AuthenticationException ex) { return Response.fail(AUTH_FAIL, "认证失败"); } }
至此,对Spring Security的异常处理模块分析完毕。
当然,
Spring Security除了上述所描述到的组件外,它还存在很多的其他组件。但,由于笔者涉猎面、章节限制等原因在这里就不在展开探讨了,有兴趣的读者可自行翻阅相关资料。
总结
通过阅读此文应该能对Spring Security快速入门了,但是Spring Security还存在好多知识点和用法,如果想进一步学习可以通过阅读官方文档和其Github上的源码。