SpringSecurity详细介绍(一)基本原理

        SpringSecurity(下文简称ss)是目前最流行的Web应用安全管理框架,其前身是Acegi项目(2006年左右纳入Spring子项目)。截至目前最新的版本是6.4.1。

        ss早期版本的配置非常复杂,项目维护小组做了大量的工作简化了框架的配置和使用,现有的版本只需要寥寥几行代码就可以实现一个基本的权限控制功能。丰富的组件和高度定制化是ss的另一大特点,官方提供了各种强大的过滤器及认证、鉴权组件,开发人员也可以按业务需求灵活定制认证和安全访问策略。ss的社区非常活跃,文档资源非常丰富,这也是ss受欢迎的原因之一。

        ss框架的部署、搭建基于spring环境,本文不会花太多篇幅讲述与spring特性相关的内容,细节之处将留在后续的博文中介绍。

        文中出现的ss源码基于版本 5.7.11,版本上的差异不太影响讲解的主题。

一、加载及工作流程

        ss处理请求的入口是一个实现了J2EE Servlet 规范的过滤器(DelegatingFilterProxy,实现了 javax.servlet.Filter 接口)。早期的项目须在web.xml中进行相关配置,web容器启动时就会加载并初始化这个过滤器。Springboot兴起后,以 ServletContainerInitializer 为基础的配置方式逐渐取代了web.xml的配置方式,Springboot项目中ss过滤器的加载流程如下图所示:

        下面是 org.springframework.web.filter.DelegatingFilterProxy 的源码:

public class DelegatingFilterProxy extends GenericFilterBean {

	@Nullable
	private WebApplicationContext webApplicationContext;

	@Nullable
	private String targetBeanName;        // 代理Bean的名称

	@Nullable
	private volatile Filter delegate;     // 实际执行请求处理的代理对象(FilterChainProxy)

    ...

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		// 延迟初始化代理对象
		Filter delegateToUse = this.delegate;
		if (delegateToUse == null) {
			synchronized (this.delegateMonitor) {
				delegateToUse = this.delegate;
				if (delegateToUse == null) {
					WebApplicationContext wac = findWebApplicationContext();
					if (wac == null) {
						throw new IllegalStateException("No WebApplicationContext found: " +
								"no ContextLoaderListener or DispatcherServlet registered?");
					}
					delegateToUse = initDelegate(wac);
				}
				this.delegate = delegateToUse;
			}
		}

		// 由代理器完成请求处理
		invokeDelegate(delegateToUse, request, response, filterChain);
	}

    ...

	protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
		String targetBeanName = getTargetBeanName();
		Assert.state(targetBeanName != null, "No target bean name set");
		Filter delegate = wac.getBean(targetBeanName, Filter.class);
        // 如果由Spring接管代理Bean的全生命周期
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}
}

        从源码可以看出,DelegatingFilterProxy 自身并不做请求处理,而是交给一个代理对象(FilterChainProxy)来完成。FilterChainProxy的加载采用了延迟执行的策略,直到第一次请求发起才执行初始化并注入到DelegatingFilterProxy对象上。

        FilterChainProxy内部也定义一条过滤器链,因此Web应用中实际有两条过滤器链,一条是基于Servlet标准的Filter过滤器链,一条是ss框架内部的虚拟过滤器链。虽然两条链中的过滤器都是继承自 javax.servlet.Filter,生命周期和执行方式却不尽相同,前者生命周期由Web容器托管,按过滤器优先级依次处理Http(s)请求;而后者由Spring容器托管,按虚拟链的排列顺序依次处理封装过的鉴权请求。

二、过滤器链

        上一节提到 FilterChainProxy 是真正处理请求的组件,它通过遍历内置的过滤器链去尝试处理Web请求。过滤器链由一串实现 Filter 接口的类组成,每个过滤器都负责处理一种特定的请求类型,如 Logout 过滤器负责处理路径为 /logout 的请求,RememberMeAuthenticationFilter 负责处理包含remember-me cookie的请求。ss支持在链中插入新的过滤器,或替换过滤器链中默认存在的过滤器。过滤器链的排列顺序影响安全校验的结果,自定义过滤器链应注意这一点。

        ss框架有一个重要的组件:令牌(Authentication)——封装了请求及访问者基本信息的载体。令牌的创建、校验、存储(或销毁),穿插在过滤器链的整个执行过程中,直至被送入最后一个过滤器(FilterSecurityInterceptor)中完成最终的授权验证。可以这样简单理解,过滤器链的执行就是为了验证令牌。

        默认的过滤器链中包含15个过滤器,上图只选取了几个比较重要的过滤器展示这个流程

三、重要组件

        强大的扩展性是ss受追捧的原因之一。官方提供大量组件满足了常见的功能开发需求,开发人员也可以根据自身业务扩展已有组件或自定义开发新的组件。

        ss提供的组件数量繁多,各个组件的功能和扩展点也非常复杂,想在一篇文章中面面俱到是难以实现的。下面的章节中,笔者对实际开发中最常用的组件做简单的介绍。

1. Authentication

        令牌是安全验证信息的载体,既可以用来封装了单次请求内容,如cookie,URL参数等;也可以放置请求发起用户的基本信息,如用户姓名、授予角色等。令牌总是存放在安全上下文(SecurityContext)中。

        过滤器在解析处理请求内容时,常会创建相应类型的令牌用于后续的校验,如登录过滤器(UsernamePasswordAuthenticationFilter)在解析请求主体内容时将传入的用户名、密码等参数封装成 UsernamePasswordAuthenticationToken ;匿名登录过滤器(AnonymousAuthenticationFilter)会根据设定的key创建 AnonymousAuthenticationToken 。这一步是安全验证流程的发起点。

        令牌具有状态属性(authenticated),校验成功的令牌该属性为真。过滤器链的处理流程中,该属性值被多次用来判断令牌的有效性,而令牌的有效性决定了流程的最终走向。权限校验通过后,令牌的状态被置为真,也有过滤器会直接新建一个新的已验证令牌替代原始令牌。

        登录鉴权的整个流程中,令牌装载的信息可能会发生变化。随着请求内容的逐步解析,令牌可以附加更多的信息用于后续的安全认证。如 AbstractAuthenticationToken 提供了 details 属性来存储除请求主体、密码、授权信息之外的内容。

        下面的例子是一个支持多种登录方式(微信、钉钉、用户名)的系统。图中流程比较简单,仅用于展示,实际场景往往要复杂的多。

        从图例可以看出,尽管不同过滤器(微信、钉钉、用户名密码)处理请求时生成的令牌不尽相同,但经过一系列的处理,这些令牌校验成功后最终转换为统一的令牌类型(UsernamePasswordAuthenticationToken),这样做有助于后续编写统一的授权验证逻辑,如果业务需要展示用户登录方式或第三方账户信息,可以在令牌转换时将第三方账户信息附加到新的令牌上。

2. AuthenticationManager

        AuthenticationManager是ss的重要组件之一,主要用于令牌校验逻辑的集中处理。前文提到了每个过滤器负责处理一类特定请求,那么最容易的做法是将请求内容解析以及令牌的校验写在一起。但如果将不同类型的令牌校验逻辑集中在一个接口中实现,就可以将请求内容解析和令牌校验逻辑解耦,AuthenticationManager 正是出于这种目的创建的。此外,FilterSecurityInterceptor在最终的授权验证前可能须再次验证令牌的有效性(由配置项决定),也需要有这样一个组件来承担这项工作。

        查看多个Filter的源码可以发现,RememberMeAuthenticationFilter、BasicAuthenticationFilter、FilterSecurityInterceptor、UsernamePasswordAuthenticationFilter等过滤器中都引入了AuthenticationManager。

        令牌校验由 AuthenticationManager 的默认实现类 ProviderManager 完成。ProviderManager内置了一个由多个校验器(AuthenticationProvider)组成的列表(List)。校验过程中,列表内的校验器按排列顺序依次执行,并将校验结果返回对应的过滤器。

        在自定义过滤器引入AuthenticationManager不是必须的,但如果你编写的自定义过滤器校验逻辑复杂且存在重用的地方,强烈建议引入这个组件。 

3. FilterSecurityInterceptor

        FilterSecurityInterceptor 过滤器位于过滤器的末尾,承担了授权验证的职能。权限框架只做两件事:身份验证和授权验证,当流程走到这个过滤器时,说明请求很可能已经通过了身份验证,框架将做最后的处理——即当前请求用户是否具备访问该网络资源的权限。

        为安全起见,FilterSecurityInterceptor 在执行授权验证前,还会再做一次身份验证(防止令牌校验结果被篡改的情况)。类成员变量 alwaysReauthenticate 状态值控制这一步操作是否执行。

        FilterSecurityInterceptor 内置了 FilterInvocationSecurityMetadataSource,这个接口提供了读取资源访问配置的方法,通过这些方法可以查询当前请求资源所须权限。

4. AccessDecisionManager

        提到 FilterSecurityInterceptor 过滤器,就不得不提访问决策管理器( AccessDecisionManager),FilterSecurityInterceptor 对资源访问许可的判断是由访问决策管理器来完成。

        我们知道授权验证需要两项数据:资源可以被哪些用户(或角色)访问、请求发起用户的身份(或角色)。前者由上一节提到的 FilterInvocationSecurityMetadataSource 提供,可以从Xml配置文件(或配置类)中读取,也可以访问关系数据库获得。后者则是在用户身份验证的过程中获得。两相对比就可以判定用户是否有权访问资源。

        访问决策管理器内置了一个投票器(AccessDecisionVoter)列表,访问决策管理器根据这些投票器遍历执行的结果决定是否允许访问,这种责任链设计模式在ss框架中被广泛应用。访问决策管理器目前有三种实现:AffirmativeBased、ConsensusBased、UnanimousBased,分别代表不同的投票策略。框架默认使用的是AffirmativeBased,它采用了一票通过的策略——即只要有任意一个投票器投票结果为通过(ACCESS_GRANTED),则该请求获得访问权。

5. ExceptionTranslationFilter

        ss的权限异常分为两大类:令牌校验异常(AuthenticationException)和访问拒绝异常(AccessDeniedException),ExceptionTranslationFilter 专用于处理过滤器链处理流程中抛出的这两类异常。其余的异常类型将被封装为 ServletException 向上抛出。

        ExceptionTranslationFilter 只能捕获排在其后的过滤器抛出的异常,由于默认情况下它处于过滤器链的倒数第二位置,意味着它只能捕获 FilterSecurityInterceptor 抛出的异常(FilterSecurityInterceptor 会执行令牌校验和授权验证,因此两类异常都可能抛出)。

        如果不想通过 ExceptionTranslationFilter 来处理权限异常,而是希望令牌验证失败后提前退出过滤器链的遍历执行,可以在过滤器内部完成异常捕获和处理,这种情况下,流程未走到倒数第二个过滤器就终止了,ExceptionTranslationFilter 也就不会被触发执行。

        ExceptionTranslationFilter 内置了两大异常处理组件:令牌处理入口(AuthenticationEntryPoint)、访问拒绝处理器(AccessDeniedHandler),分别用于处理令牌校验和鉴权的异常,这两个组件也被引入了其它过滤器,这体现了ss的灵活性。

6. SecurityContextRepository

        安全上下文(SecurityContext)用来装载令牌,安全上下文仓库(SecurityContextRepository)则是用来加载和存储安全上下文。这个接口有多种实现,框架默认使用 HttpSessionSecurityContextRepository(即使用HttpSession存取安全上下文的实现),但在现实中更多采用Redis等缓存工具实现。

        过滤器 SecurityContextHolderFilter(早期版本为 SecurityContextPersistenceFilter)与SecurityContextRepository 紧密相关。为了避免过滤器链处理过程中频繁读写仓库中的令牌信息,SecurityContextHolderFilter 在过滤器链执行初期一次性将安全上下文从仓库中读取出来并放入一个Threadlocal对象(SecurityContextHolder),这样后续的过滤器可以直接访问这个Threadlocal对象,而不必再去读取仓库,既保障了上下文信息的安全性,也加快了存取速度。鉴权成功后再更新一次安全上下文,并清除Threadlocal内容即可。

7. 事件

        ss的事件处理基于Spring自带的事件机制。ss框架中的事件分为两大类型:令牌验证事件(AbstractAuthenticationEvent)、授权验证事件(AbstractAuthorizationEvent),前者代表身份验证过程中的各种事件,如校验成功、退出登录等;后者代表用户权限检查过程中的各种事件,如授权通过、授权拒绝。

        下图描述了安全事件类的继承关系:

        开发人员可以根据需求自定义安全事件,通过 ApplicationContext 发布事件,并注册监听器(ApplicationEventListener)完成事件的处理。

四、小结

        Springsecurity的组件远比本文提到的要多,功能点更是繁多。笔者只选择了本人日常开发中常用的做简单介绍,想要了解这个框架的全貌,还需要仔细阅读源码,尝试做一些定制化的开发。感兴趣的同学可以从下面的源码入口为起点,逐步调试观察每个过滤器的具体实现逻辑,这也是熟练掌握ss框架的必经之路。

public class FilterChainProxy extends GenericFilterBean {
    
    ...	

    private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
		HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
		List<Filter> filters = getFilters(firewallRequest);
		if (filters == null || filters.size() == 0) {
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
			}
			firewallRequest.reset();
			chain.doFilter(firewallRequest, firewallResponse);
			return;
		}
		if (logger.isDebugEnabled()) {
			logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
		}
		VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
		// 过滤器链调用发起点
        virtualFilterChain.doFilter(firewallRequest, firewallResponse);
	}
}

        

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值