Spring Security 基本套路

原理:

在 Spring Web 项目中,基于过滤器处理认证与授权,在过滤器中调用认证管理器 AuthenticationManager 与 授权管理器 accessDecisionManager 进行认证及授权

自动配置:

Spring boot 通过 SecurityAutoConfiguration 给项目提供了默认的安全配置,一个用户名为 'user' 的用户,随机密码并再启动时打印到控制台,所有请求都需要通过认证。

自定义配置:

如果要移除默认的行为,自定义安全策略,则可以提供一个 继承 WebSecurityConfigurerAdapter 并注解 @Configuration 与 @EnableWebSecurity 

@EnableWebSecurity

作用是创建下图中的 FilterChainProxy ,并提供一些基本的安全防护,如 CSRF attack prevention 、Session Fixation protection 等,具体查看官方文档

Spring Security 基本套路

WebSecurityConfigurerAdapter.class 

该类中提供一个默认配置方法 protected void configure(HttpSecurity http) throws Exception 自定义策略则需重写该方法。

HttpSecurity

该类是 SecurityBuilder 接口的实现类,是一个通过 http request 方式配置 Web 安全的构造器,与使用 xml <http> 配置的方式相似

调用 HttpSecurity 的方法实际上就是在进行配置,如:authorizeRequests() formLogin() 分别会创建 ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer ,它们是 SecurityConfigurer 接口的实现类,意思差不多就是不同的安全配置器。每一个配置器 (SecurityConfigurer) 又会对应创建一个或多个过滤器 (可查看 api 或 源码有说明),上面的配置器对应创建了 FilterSecurityInterceptor、UsernamePasswordAuthenticationFilter。

FilterSecurityInterceptor (在授权流程中有详细介绍)

这个过滤器比较重要,负责 Http 资源的安全性 (responsible for handling the security of HTTP resources),需要依赖 AuthenticationManager、AccessDecisionManager、FilterInvocationSecurityMetadataSource。

重点解析一下认证的基本流程:

使用 InMemoryUser 的方式

入口:UsernamePasswordAuthenticationFilter
该类继承自 AbstractAuthenticationProcessingFilter ,在父类中将注入 authenticationManager、拦截请求、处理认证成功与失败后的逻辑
UsernamePasswordAuthenticationFilter 定义了请求 Url ,并实现了父类的抽象方法 attemptAuthentication ,拦截到请求后,调用 attemptAuthentication 方法尝试认证。

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password)
通过获取的请求参数,构造一个 Authentication,且被标记为未认证 ( setAuthenticated(false) )
不同的认证方式会构造不同的 token , 这些 token 的实现类都继承自 AbstractAuthenticationToken
而 AbstractAuthenticationToken 则实现了 Authentication ,即所有的 token 也是 Authentication 的实例 。

AbstractAuthenticationToken 的其他实现:

Spring Security 基本套路

setDetails(request, authRequest)
该方法为 UsernamePasswordAuthenticationToken 的父类 AbstractAuthenticationToken 填充了 details 属性 (主要就是 remoteAddress 与 sessionId)
通过调用 WebAuthenticationDetailsSource 的 buildDetails 方法来完成,该类是 AuthenticationDetailsSource 接口的其中一个实现类 。

this.getAuthenticationManager().authenticate(authRequest)
将认证的具体工作交给 AuthenticationManager,该接口只有一个 authenticate 方法

ProviderManager

该类是 AuthenticationManager 的实现,这里实际上是调用 ProviderManager 的 authenticate 方法
该方法循环在系统中定义或配置的 AuthenticationProvider, 通过 supports 方法判断 UsernamePasswordAuthenticationFilter 构造的 Authentication 是哪个 provider 所支持的 。

AuthenticationProvider

该接口只有两个方法, authenticate 用于认证, supports  用于判断是否支持验证该 Authentication

AuthenticationProvider 有如下实现类:

Spring Security 基本套路

在这里最终匹配到 DaoAuthenticationProvider,该类的父类 AbstractUserDetailsAuthenticationProvider 的 supports 方法接受 UsernamePasswordAuthenticationToken.class

Spring Security 基本套路

匹配后最终将调用 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法进行验证
该方法首先尝试从 cache 中获取用户信息 UserDetails ( UserDetails user = this.userCache.getUserFromCache(username) )
如果没有则通过 UserDetailsService 接口的 loadUserByUsername 获取 UserDetails ,UserDetails 只是一个接口,定义了一些对用户信息的操作方法,User 类实现了 UserDetails 。

DaoAuthenticationProvider

该类实现了父类 AbstractUserDetailsAuthenticationProvider 的抽象方法 retrieveUser ,该方法调用 UserDetailsService 接口的 loadUserByUsername 方法获取用户信息 ( this.getUserDetailsService().loadUserByUsername(username) )
在这里 loadUserByUsername 方法由 InMemoryUserDetailsManager 实现,该类实现了 UserDetailsManager 接口,而 UserDetailsManager 接口则继承自 UserDetailsService
AbstractUserDetailsAuthenticationProvider 还要一个抽象方法 additionalAuthenticationChecks
文档中指出:
打多数情况下,retrieveUser 方法并不履行密码的检验,而是在 additionalAuthenticationChecks 方法进行,如果需要比较 UserDetails 与 UsernamePasswordAuthenticationToken 中的其他的属性,也将在该方法中进行

上面的认证逻辑走完后:

UsernamePasswordAuthenticationFilter 返回一个标记为认证成功的 Authenticate (UsernamePasswordAuthenticationToken)

如果认证成功:

则调用 AbstractAuthenticationProcessingFilter 的 successfulAuthentication 方法,将认证后的 Authentication 存储到 SecurityContextHolder 中,处理 RememberMe 逻辑,然后再调用SimpleUrlAuthenticationSuccessHandler 的 onAuthenticationSuccess 方法跳转视图
Srping 默认使用 ThreadLocalSecurityContextHolderStrategy 策略通过 ThreadLocal 来存储 SecurityContextHolder,这意味着在该线程中(一个请求)任意地方都可以访问 SecurityContextHolder

如果认证失败:

则调用 unsuccessfulAuthentication 方法, 该方法首先清除 SecurityContextHolder ,然后调用 AuthenticationFailureHandler 接口的实现类 SimpleUrlAuthenticationFailureHandler 的 onAuthenticationFailure 方法,在该方法主要完成两个操作,保存异常信息与跳转。

保存异常信息 saveException

通过 forwardToDestination 属性判断保存方式,如果 true , 则保存在 request 中

request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);

如果 false 则保存在 session 中

request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);

AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION"

跳转同样通过 forwardToDestination 判断是用 forward 还是 sendRedirect

授权的基本流程:

入口过滤器是 FilterSecurityInterceptor,该类继承自 AbstractFilterSecurityInterceptor ,核心逻辑在 beforeInvocation 方法中,主要负责三个工作

获取访问该资源(请求url)所需的角色列表

系统将定义(configure中硬编码或xml配置)的资源与角色关系存储在一个 requestMap 中

Map<RequestMatcher, Collection<ConfigAttribute>> requestMap

该 Map 的 key 即 RequestMatcher 则是资源 url ,value 即 Collection<ConfigAttribute> 则是角色的集合 (一个资源对应多个角色)

调用 DefaultFilterInvocationSecurityMetadataSource.getAttributes() 方法用请求的 url 去匹配 Map 中的 key ,如果匹配上则返回角色列表

获取已认证用户 Authentication

从 SecurityContextHolder 中获取 Authentication 

判断用户角色与资源所需角色是否相等

调用 accessDecisionManager 实现类的 decide 方法,decide 方法内调用投票器 (Voter) 进行判断

在投票器中获取认证用户的角色 getAuthorities()  ,并与第一步中取得的资源所需的角色列表进行对比后进行授权

实现细粒度的权限控制:

思路1:

new 一个 FilterSecurityInterceptor , 并注入自定义的 FilterInvocationSecurityMetadataSource (从数据库获取权限集合) 、AccessDecisionManager (授权决策、判断),用新建的 filter 替换原 filter , http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)

思路2:

使用 ObjectPostProcessor 配置 FilterSecurityInterceptor 的相关属性,使用自定义的 FilterInvocationSecurityMetadataSource (从数据库获取权限集合) 与 AccessDecisionManager (判断是否授权)

心得:

自定义一个拦截器来处理授权感觉更方便,只不过脱离了 Security 的授权架构了。

还有原因需要考虑:

Security 通过 filter 在启动时加载权限集合,如果有变动岂不是要重启项目;

处理逻辑和目前项目有区别,Security 首先要为资源配置角色,再取出访问资源授予的全部角色与当前用户拥有的角色进行比较,而目前项目一般是对角色赋予可访问的资源,并未对每个资源提前配置角色集合;

这两点不符合目前的权限控制方式,Security 的方式不够灵活

一些额外的:

ThreadLocal 是什么:

就是 java.lang.ThreadLocal ,线程本地变量,简单的理解,对于当前线程来说,它是一个全局变量,在当前线程的任意地方都可以访问该变量中存储的值;对于多线程来说,它又是独立的,每个线程有自己的 ThreadLocal 变量,其他线程不能访问。

SecurityContextHolder 的一些解析:

在 Web Application 中,使用 ThreadLoacl 存储 SecurityContextHolder 策略提供安全的上下文,Spring security 通过 SecurityContextPersistenceFilter 这个过滤器将上下文存储在 HttpSession 中,用于恢复上下文到 SecurityContextHolder 中,因为一个请求完成后,Spring security 将清除 SecurityContextHolder, 为了安全起见,Spring 不建议直接与 HttpSession 交互,都是通过 SecurityContextHolder 进行替代。

官方文档对于这几个类的总结:

SecurityContextHolder, to provide access to the SecurityContext.
SecurityContext, to hold the Authentication and possibly request-specific security information.
Authentication, to represent the principal in a Spring Security-specific manner.
GrantedAuthority, to reflect the application-wide permissions granted to a principal.
UserDetails, to provide the necessary information to build an Authentication object from your application’s DAOs or other source of security data.
UserDetailsService, to create a UserDetails when passed in a String-based username (or certificate ID or the like).