简介
Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架,它能够在web请求级别和方法调用级别处理身份认证和授权。由于是基于Spring框架,它充分地利用了依赖注入和面向切面技术
身份认证和授权(Authentication and Authorization)
应用程序安全性差不多可以归结为两个独立的问题:
身份认证(Authentication)(你是谁)
授权(Authorization)(你可以做什么?)。
有时人们会说“访问控制”而不是“授权”,这可能会造成困惑,但是以这种方式思考可能会有所帮助,因为“授权”在其他地方又有其他含义。 Spring Security的体系结构旨在将身份认证与授权分开,并且具有许多策略和扩展点
源代码
版本: 5.3.x
下载地址 https://github.com/spring-projects/spring-security.git
Servlet Security
Servlet Filters
Spring Security 对于Servlet的支持是基于Servlet Filters。
下图描述的是典型的Servlet Filter 对于HTTP Request的处理
当客户端的请求到达, Servlet 会按照请求的URI,依序将请求交由FilterChain的Filter进行处理
在Spring MVC中的Servlet 是 DispatcherServlet
DelegatingFilterProxy
Servlet 容器支持自定义的Filter,但是不知道Bean的定义, 所有Spring 提供了一个 Filter DelegatingFilterProxy, 它的主要作用是把容器的生命周期和Spring的ApplicationContext
DelegatingFilterProxy 是基于Servlet 容器的机制注册的,然后把具体的工作代理给继承了Filter 的Spring Bean
DelegatingFilterProxy Pseudo Code
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);
}
FilterChainProxy
- Spring Security 对于Servlet的支持是在 FIlterChainProxy 中实现。
- FIlterChainProxy 是一个特殊的Filter,封装在 DelegatingFilterProxy 里面
- FIlterChainProxy 决定了调用 SecurityFilterChain 里面的哪一个 Security Filter
SecurityFilterChain
在SecurityFilterChain 里面注册是SecurityFilter,他们是通过FilterChainProxy注册,而不是DelegatingFilterProxy
Multiple SecurityFilterChain
通过自定义的FilterChainProxy,可以定义多个SecurityFilterChain
Security Filters
下面是默认的Spring Security Filters 及其顺序
- ChannelProcessingFilter
- ConcurrentSessionFilter
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- CsrfFilter
- LogoutFilter
- OAuth2AuthorizationRequestRedirectFilter
- Saml2WebSsoAuthenticationRequestFilter
- X509AuthenticationFilter
- AbstractPreAuthenticatedProcessingFilter
- CasAuthenticationFilter
- OAuth2LoginAuthenticationFilter
- Saml2WebSsoAuthenticationFilter
- UsernamePasswordAuthenticationFilter
- ConcurrentSessionFilter
- OpenIDAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- DigestAuthenticationFilter
- BearerTokenAuthenticationFilter
- BasicAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- JaasApiIntegrationFilter
- RememberMeAuthenticationFilter
- AnonymousAuthenticationFilter
- OAuth2AuthorizationCodeGrantFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
- SwitchUserFilter
Security Exceptions
ExceptionTranslationFilter 可以处理两种类型的异常
- AccessDeniedException
- AuthenticationException
ExceptionTranslationFilter pseudo code
try {
filterChain.doFilter(request, response); // step1
} catch (AccessDeniedException | AuthenticationException e{
if (!authenticated || e instanceof AuthenticationException) {
startAuthentication(); //step2
} else {
accessDenied(); //step3
}
}
Authentication Architecture Components
SecurityContextHolder
为当前线程创建了一个 ThreadLocal 用于存放当前的 SecurityContext,既是当前 Spring Security 的上下文,包含对象,参数等任何一切与 Spring Security 相关的内容;
SecurityContext
通过SecurityContextHolder获得当前认证user 的信息
Authentication
org.springframework.security.core.Authentication 是 java.security.Principal 的继承接口,提供了 Spring Security 额外需要的用户认证相关的接口信息,比如 Credentials,Details information,is Authenticated 等等信息;
GrantedAuthority
权限
AuthenticationManager
AuthenticationManager 提供了认证方法的入口,并且它只实现了一个用于认证的方法,接收 Authentication 对象作为验证的参数;
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
ProviderManager
它是 AuthenticationManager 的一个实现类,提供了基本的认证逻辑和方法;不过为了便于灵活扩展,它包含了一个 List 对象,通过 AuthenticationProvider 接口来扩展出不同的认证提供者;
AuthenticationProvider
该对象主要用来为 ProviderManager 扩展出不同的认证提供者既 Providers
Spring Security 验证逻辑
通过上面类图,
下半部黄色部分,提供了一系列的类和功能用来支撑对Authentication对象的验证
上半部分,介绍了Authentication的组成,该部分主要是将用户输入的用户名和密码进行封装,封装成Authentication对象,并提供了AuthenticationManager进行验证;验证完成后返回一个认证成功的Authentication对象
Provider Manager
ProviderManager通过实现AuthenticationManager接口方法 authenticate() 实现验证逻辑,主要流程包含三个方面,
-
遍历所有的 Providers,然后依次执行该 Provider 的验证方法
- 如果某一个 Provider 验证成功,则跳出循环不再执行后续的验证;
- 如果验证成功,会将返回的 result 既 Authentication 对象进一步封装为 Authentication Token;比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等;这些 Authentication Token 也都继承自 Authentication 对象;
-
如果 #1 没有任何一个 Provider 验证成功,则试图使用其 parent Authentication Manager 进行验证;
-
是否需要擦除密码等敏感信息;
DaoAuthenticationProvider
DAO,英文全名 Data Access Object,数据访问对象
DaoAuthenticationProvider 是接口AuthticationProvider的一个实现类
DaoAuthenticationProvider 包含一个属性UserDetailService
DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider主要做了三件事情,
- 对用户身份信息进行加密操作;主要是传入一个PasswordEncoder对象
private PasswordEncoder passwordEncoder;
- 实现了 AbstractUserDetailsAuthenticationProvider 预留的两个扩展点,
- 获取用户信息的扩展点
private UserDetailsService userDetailsService;
...
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
....
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
可见,主要是通过注入 UserDetailsService 接口对象,并调用其接口方法 loadUserByUsername(String username) 获取得到相关的用户信息;正如类图中我们所看到的那样,UserDetailsService接口非常重要,接入了 JdbcDaoImpl,InMemoryUserDetailsManager 等等用户来源接口实现类;
- 实现相关的 additionalAuthenticationChecks 的额外验证方法;
该抽象方法是 AbstractUserDetailsAuthenticationProvider 提供给子类的可扩展的核心入口方法,我们看看 DaoAuthenticationProvider 是怎么做的
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
- AbstractUserDetailsAuthenticationProvider 为DaoAuthenticationProvider提供了基本的认证方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
...
// 1. 获取用户
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
// 2.1 pre-authenticate
preAuthenticationChecks.check(user);
// 2.2 additional-authenticate
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
...
// 2.3 post-authenticate
postAuthenticationChecks.check(user);
...
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 3. 封装通过验证的用户信息
return createSuccessAuthentication(principalToReturn, authentication, user);
}
通过省略后的代码,主要验证逻辑有以下几步
- 获取用户,通过用户名去获得用户的信息并作为UserDetails对象进行返回;
AbstractUserDetailsAuthenticationProvider定义了一个抽象的方法
protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
- 验证
- preAuthenticationChecks
提供了对用户基本信息的一些默认的前置验证逻辑,包括,用户账户是否被锁定,是否是 enabled 的状态以及用户账户是否过期等逻辑的验证; - additionalAuthenticationChecks
这是一个扩展点,该方法是一个抽象方法,必须由子类进行实现, - postAuthenticationChecks
提供了对用户基本信息的一些默认的后置验证逻辑,默认实现很简单,就是对用户的 Credential 既密码进行判断,判断其是否过期。
- 最后,将已通过验证的用户信息封装成 UsernamePasswordAuthenticationToken 对象并返回;该对象封装了用户的身份信息,以及相应的权限信息,相关源码如下,
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
总结:
从这里我们就可以清晰的看到,UserDetailsService接口作为桥梁,是DaoAuthenticationProvier与特定用户信息来源进行解耦的地方,UserDetailsService由UserDetails和UserDetailsManager所构成;UserDetails和UserDetailsManager各司其责,一个是对基本用户信息进行封装,一个是对基本用户信息进行管理;
特别注意,UserDetailsService、UserDetails以及UserDetailsManager都是可被用户自定义的扩展点,我们可以继承这些接口提供自己的读取用户来源和管理用户的方法,比如我们可以自己实现一个 与特定 ORM 框架,比如 Mybatis 或者 Hibernate,相关的UserDetailsService和UserDetailsManager;
时序图
Authorization
在方法或者Web 请求之前的, Spring Security提供了拦截器通过AccessDecisionManager来检查用户权限
决策管理器
Spring Security已经内置了几个基于投票的AccessDecisionManager,
- AffirmativeBased
- ConsensusBased
- UnanimousBased
当然如果需要你也可以实现自己的AccessDecisionManager
Spring Security内置了三个基于投票的AccessDecisionManager实现类,它们分别是AffirmativeBased、ConsensusBased和UnanimousBased。
AffirmativeBased的逻辑是这样的:
(1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。
ConsensusBased的逻辑是这样的:
(1)如果赞成票多于反对票则表示通过。
(2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。
(3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。
(4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。
UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。UnanimousBased的逻辑具体来说是这样的:
- 在Authentication 之后,AuthenticationManager 会将用户的GrantedAuthority 插入到Authentication
GrantedAuthority 是一个接口,只有一个方法
投票者
RoleVoter是Spring Security内置的一个AccessDecisionVoter,其会将ConfigAttribute简单的看作是一个角色名称,在投票的时如果拥有该角色即投赞成票。如果ConfigAttribute是以“ROLE_”开头的,则将使用RoleVoter进行投票。当用户拥有的权限中有一个或多个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute时其将投赞成票;如果用户拥有的权限中没有一个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute,则RoleVoter将投反对票;如果受保护对象配置的ConfigAttribute中没有以“ROLE_”开头的,则RoleVoter将弃权。
AuthenticatedVoter也是Spring Security内置的一个AccessDecisionVoter实现。其主要用来区分匿名用户、通过Remember-Me认证的用户和完全认证的用户。完全认证的用户是指由系统提供的登录入口进行成功登录认证的用户。
基于Web 的Authorize 流程
-
首先FilterSecurityInterceptor 从SecurityContextHolder中获得一个Authentication对象
-
FilterSecurityInterceptor 创建从HttpServletRequest, HttpServletResponse, and FilterChain 中创建一个 FilterInvocation, 然后传给FilterSecurityInterceptor.
-
把FilterInvocation 传给SecurityMetadataSource 以获得ConfigAttributes.
-
把Authentication, FilterInvocation, 和 ConfigAttributes 传入AccessDecisionManager.
-
如果authorization is denied, 抛出 AccessDeniedException, 然后ExceptionTranslationFilter 处理这个AccessDeniedException.
-
如果访问授权,FilterSecurityInterceptor 继续FilterChain的处理