Spring Security快速入门
一文带你快速走进
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 | 用于负责用户上下文的切换(切换角色等)。 |
相关知识点
CORS
:详情可阅读 Wiki《Cross-origin resource sharing》Csrf
:详情可阅读 Wiki《Cross-site request forgery》Digest
:详情可阅读 Wiki《Digest access authentication》Basic
:详情可阅读 Wiki《Basic access authentication》JAAS
:详情可阅读 Wiki《Java Authentication and Authorization Service》
至此,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
认证信息并计算出密钥串进行认证处理。其中,
Basic Authentication
与Digest Authentication
的差异可阅读以下资料:
根据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
上的源码。
参考
- Spring《Spring Security官方文档》
- Wiki《Cross-origin resource sharing》
- Wiki《Cross-site request forgery》
- Wiki《Digest access authentication》
- Wiki《Basic access authentication》
- Wiki《Java Authentication and Authorization Service》
未经本人许可,禁止转载
转载自:https://juejin.cn/post/7168389163778048008