Spring Security 笔记(长期)

参考资料:

Spring Security 参考手册

源码(Spring Security)

开源框架案例源码(RuoYi)

引言

最近工作折腾的项目,合作公司二次开发了 RuoYi 框架,大部分代码是纯业务流的东西。

不过也还算是涉及到了 Spring Security 的使用,以及数据权限改造设计。

RuoYi 算是一个大而全的框架,作为模版工程能减少许多麻烦,但是拿来主义也是不可取的。于是想着不如结合 RuoYi 的部分源码,在本地构建一下 Spring Security,做个学习总结。

Security 构建

按照 github 上的流程进行构建即可。

遇到了以下问题,可留意:

  • JAVA_HOME 对应 jdk11
  • 选取合适的 Gradle 版本
  • Gradle 构建时可以使用 Proxy 便于下载所需资源
  • 请认真参阅 README

结构概述

Security 的核心还是过滤器链,使用责任链模式。

首先,在没有 Spring Security 的情况下,我们通常会选择自己实现 Filter 来处理请求。如果情况复杂一点的话,我们也可以链式调用 Filter。

Security 框架的主要作用,就是帮我们设想了各种环境下可能遇到的情况,并抽离出接口。我们由此可以按需配置过滤器链,下面会稍微详细一点的描述。

首先,别去关注某个 Filter 的细节,而是切换到较为全面的尺度 —— 要配置过滤器链,就需要我们定义规则将 Filter 们按照一定顺序组织起来,因此我们需要一个类来指导全局。

以 Web 举例,我可以配置一个 WebSecurityConfig (当然这个名字你可以自己定义),继承 Security 提供的抽象类 WebSecurityConfigurerAdapter 。然后,要根据 Security 的要求,实现继承抽象类的一些方法 —— 比如说,这个抽象类最关键的方法 —— configure(HttpSecurity http)

实现这个方法,就是在组织各个 Filter 之间的顺序、部分准入逻辑、组件的启停。

假设整个过滤器链是一辆火车,Filter 和一些功能模块是车厢,请求从火车头的车门进入,经过一节一节车厢的检测,最后在某个车厢(或是顺利走到车尾车厢)下车。

那么这个整体的 ConfigurerAdapter 就是整辆火车(而不是单个车厢)的设计图。

OK,通过 WebSecurityConfigurerAdapter,我们可以从抽象程度较高的层面来组织流程。但是请注意,具体 Filter 的执行逻辑还没实现。而 ConfigurerAdapter 中,我们配置的是 Filter 的 Configurer (Filter 的功能是抽象的,还不会关注 Filter 的具体实现)

想像我们在整个火车的设计图里,在对应车厢的位置(我们总不能放上一个真的车厢),用图钉钉上这个车厢单独的设计图。

这个单个车厢的设计图,就是 Filter 的配置类。例如,自定义一个用户登录过滤器配置类 UserLoginConfigurer,用来配置自定义的用户登录过滤器 UserAuthenticationFilter。

这个 UserLoginConfigurer,继承 AbstractHttpConfigurer(也就是 Security 帮我们预先设计好的抽象类)。

根据 AbstractHttpConfigurer 的要求,需要实现 configure() 方法,同理,这个方法帮助我们自定义配置 Filter 的策略和组件。比如:

  • AuthenticationManager,用于注册和分配 Provider
  • AuthenticationSuccessHandler / AuthenticationFailureHandler,表示认证成功/失败情况的处理器。
  • SessionAuthenticationStrategy,提供会话认证策略

由此,整个过滤器链可以以类似的逻辑从粗到细一步一步配置完毕。

功能源码解析

Filter

Filter 是 Servlet 的基础接口,要了解过滤器接口,我们可以阅查 Filter.java 的 javadocs

A filter is an object that performs filtering tasks on either the request to a resource (a servlet or static content), or on the response from a resource, or both.

过滤器是一个对象,参与 request 和 response 单或双端的过滤任务。

Filters perform filtering in the doFilter method. Every Filter has access to a FilterConfig object from which it can obtain its initialization parameters, and a reference to the ServletContext which it can use, for example, to load resources needed for filtering tasks.

过滤器通过 doFilter 方法执行过滤行为。所有过滤器都需要接入 FilterConfig 对象,从中获取参数、引用、过滤任务所需的资源。

例举设计:
1. Authentication Filters
2. Logging and Auditing Filters
3. Image conversion Filters
...

实现 Filter 接口只需要满足 init(), destroy(), doFilter() 三个方法。同时在初始化时需要传入 FilterConfig 对象。

doFilter() 允许检查请求,控制输入输出过滤,选择并调用下一个过滤器或者阻止请求,设定标头等。


Abstract Security Interceptor

实现安全拦截的抽象类。作为抽象类,首先要确保安全拦截器的配置正确。

持有成员和大致功能如下:

  1. MessageSourceAccessor 获取 messages
  2. ApplicationEventPublisher 事件发布
  3. AccessDecisionManager 控制授权策略
  4. AfterInvocationManager 审查并修改从安全对象调用返回的对象
  5. AuthenticationManager 处理身份认证请求
  6. RunAsManager 为当前安全对象调用创建临时令牌

安全拦截的抽象类中的几个较为重要的方法:

  1. IntercptorStatusToken beforeInvocation(Object object)
  2. void finallyInvocation(IntercptorStatusToken token)
  3. Object afterInvocation(IntercptorStatusToken token, Object returnedObject)

其调用顺序 before -> invocation -> finally -> after (finally方法并不只是在 finally 代码块中调用,在 after 方法内部的靠前位置也存在调用)。

beforeInvocation()

  • 1. 通过子类重写的 obtainSecurityMetadataSource() 方法去获取当前 object 的 attributes (配置的特性集合)。
  • 2. authenticateIfRequired() 去上下文中获取令牌,并对令牌进行验证:
    • 已认证就直接返回。
    • 未认证就新建上下文,通过 authenticationManager 建立新的令牌,并将新的上下文注入 SecurityContextHolder,返回新建的令牌。
  • 3. attempAuthorization() 尝试授权,通过 AccessDecisionManager.decide() 进行决策。这里会抛出拒绝访问异常,拒绝访问异常包括:
    • AccessDeniedException – 身份验证不具备所需的权限或 ACL 特权
    • InsufficientAuthenticationException – 身份验证没有提供足够的信任级别
  • 4. 通过 RunAsManager 另外创建一个令牌 runAs(需要临时替换令牌的情况)。
    • 如果不需要临时替换令牌,则返回 null,之后返回安全拦截状态标记,contextHolderRefreshRequired 为 false。
    • 如果需要临时令牌(使用 runAs),创建新的上下文 newCtx 并注入 runAs,将 newCtx 存入SecurityContextHolder 中。返回安全拦截状态标记, contextHolderRefreshRequired 为 true。

finallyInvocation()

如果在 beforeInvocation() 方法中使用了 RunAsManager 创建的临时令牌,则要刷新当前 SecurityContextHolder 中的 context。

aflterInvocation()

  • token 判空。
  • finallyInvocation()
  • 使用 AfterInvocationManager.decide(),传入调用详细信息 (包括返回的对象),执行授权决策逻辑,或选择性地修改返回的对象。

Filter Security Interceptor

FilterSecurityInterceptor 即继承了 AbstractSecurityInterceptor,并且实现了 Filter 接口。

之所以先查看上面的接口和抽象类,意图就是从 FileterSecurityInterceptor 的实现入手。

FilterSecurityInterceptor 重写的 doFilter 方法,仅调用了 invoke()。而 invoke() 方法内部主要内容即为 beforeInvocation(), finallyInvocation(), afterInvocation()。

public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
	// 第一次进入时,此处 isApplied 应返回 false
	// 如果过滤器已应用过当前请求,每个请求处理一次,可以不再安全认证
	if (isApplied(filterInvocation) && this.observeOncePerRequest) {
		// filter already applied to this request and user wants us to observe
		// once-per-request handling, so don't re-do security checking
		filterInvocation.getChain().doFilter(filterInvocation.getRequest(), 
                    filterInvocation.getResponse());
		return;
	}
	// 第一次进入当前请求,进行安全认证(并修改 attribute)
	// first time this request being called, so perform security checking
	if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
		filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
	}
	InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
	try {
		filterInvocation.getChain().doFilter(filterInvocation.getRequest(), 
                    filterInvocation.getResponse());
	}
	finally {
		super.finallyInvocation(token);
	}
	super.afterInvocation(token, null);
}

到这里 FilterSecurityInterceptor 执行的抽象流程已经梳理的差不多了。但是我们注意到 beforeInvocation() 最关键的部分 —— AccessDecisionManager 依然是一个接口,其 device() 的逻辑还不清楚。

IDEA 2022 可以快速找到当前接口的实现类:

可以看到,由 AccessDecisionManager 接口实现了一个抽象类 AbstractAccessDecisionManager,继承此抽象类实现了三种策略不同的子类:

  • AffirmativeBased 基于肯定策略,任意 Voter 返回同意,则授权
  • ConsensusBased 基于共识策略,多数 Voter 返回同意(忽略弃权),则授权
  • UnanimousBased 基于一致性策略,所有 Voter 返回同意(同意数量需要大于0,忽略弃权),则授权

具体的 decide 内部逻辑并不复杂,这里就拿 UnanimousBased 的代码举例:

// UnanimousBased#decide
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)
		throws AccessDeniedException {
	int grant = 0;
	// 生成 capacity 为 1 的 attribute List,因为 voter.vote 方法对入参有要求
	List<ConfigAttribute> singleAttributeList = new ArrayList<>(1);
	singleAttributeList.add(null);
	for (ConfigAttribute attribute : attributes) {
		singleAttributeList.set(0, attribute);
		// 遍历所有 voter
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, singleAttributeList);
			switch (result) {
			// 同意则 grant 增加
			case AccessDecisionVoter.ACCESS_GRANTED:
				grant++;
				break;
			// 只要出现 deny 就抛出异常
			case AccessDecisionVoter.ACCESS_DENIED:
				throw new AccessDeniedException(
						this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", 
							"Access is denied"));
			default:
				break;
			}
		}
	}
	// To get this far, there were no deny votes
	if (grant > 0) {
		return;
	}
	// To get this far, every AccessDecisionVoter abstained
	checkAllowIfAllAbstainDecisions();
}

这时关注点又到了新出现的 AccessDecisionVoter 中了。与 Manager 类似,Voter 也是一个接口,实现了多个子类:

随便查看一个实现,例如我们进入 AuthenticatedVoter,查看 javadocs:

Votes if a ConfigAttribute.getAttribute() of IS_AUTHENTICATED_FULLY or IS_AUTHENTICATED_REMEMBERED or IS_AUTHENTICATED_ANONYMOUSLY is present. 

如果 
IS_AUTHENTICATED_FULLY
IS_AUTHENTICATED_REMEMBERED
IS_AUTHENTICATED_ANONYMOUSLY
存在,则投票。

This list is in order of most strict checking to least strict checking.

检查顺序为 FULLY -> REMEMBERED -> ANONYMOUSLYThe current Authentication will be inspected to determine if the principal has a particular level of authentication. 

当前令牌将会被检查,从而确定主体是否具有特定级别的权限。

The "FULLY" authenticated option means the user is authenticated fully.
(i.e. 
    AuthenticationTrustResolver.isAnonymous(Authentication) is false,
    AuthenticationTrustResolver.isRememberMe(Authentication) is false
). 
【仅完全认证 能通过】

The "REMEMBERED" will grant access if the principal was either authenticated via remember-me OR is fully authenticated. 
【REMEMBER-ME 认证、完全认证,都能通过】

The "ANONYMOUSLY" will grant access if the principal was authenticated via remember-me, OR anonymously, OR via full authentication.
【匿名认证、REMEMBER-ME 认证、完全认证,全通过】

可以看到, voter 会处理对应的 attribute

Filter Chain Proxy

Delegates Filter requests to a list of Spring-managed filter beans.

将过滤器请求委托给 Spring 管理的过滤器 bean 列表。

The FilterChainProxy is linked into the servlet container filter chain by adding a standard Spring link DelegatingFilterProxy declaration in the application web.xml file.

通过在应用程序 web.xml 文件中添加标准 Spring 链接 DelegatingFilterProxy 声明,将 FilterChainProxy 链接到 servlet 容器过滤器链。

---

配置FilterChainProxy is configured using a list of SecurityFilterChain instances, each of which contains a RequestMatcher and a list of filters which should be applied to matching requests. Most applications will only contain a single filter chain, and if you are using the namespace, you don't have to set the chains explicitly.

FilterChainProxy 使用一组 SecurityFilterChain 实例进行配置,每个实例都包含一个 RequestMatcher 和一个过滤器列表,这些过滤器应该应用于匹配的请求。大多数应用程序只包含一个过滤器链,如果您使用命名空间,则不必显式设置链。

---

请求处理Each possible pattern that the FilterChainProxy should service must be entered. The first match for a given request will be used to define all of the Filters that apply to that request. This means you must put most specific matches at the top of the list, and ensure all Filters that should apply for a given matcher are entered against the respective entry. The FilterChainProxy will not iterate through the remainder of the map entries to locate additional Filters.

必须输入每个 FilterChainProxy 可能服务的模式。第一个符合给定请求的匹配项将用于定义适用于该请求的所有过滤器。这意味着您必须将最具体的匹配项放在列表的顶部,并确保应适用于给定匹配器的所有过滤器都是针对相应条目输入的。 FilterChainProxy 不会遍历剩余的映射条目来定位其他过滤器。

---

请求防火墙An HttpFirewall instance is used to validate incoming requests and create a wrapped request which provides consistent path values for matching against. See StrictHttpFirewall, for more information on the type of attacks which the default implementation protects against. A custom implementation can be injected to provide stricter control over the request contents or if an application needs to support certain types of request which are rejected by default.

HttpFirewall 实例用于验证传入请求并创建一个包装请求,该请求提供一致的路径值以供匹配。请参阅 StrictHttpFirewall,以获取有关默认实现所防御的攻击类型的更多信息。可以注入自定义实现以对请求内容提供更严格的控制,或者如果应用程序需要支持某些类型的请求,这些请求默认会被拒绝。

FilterChainProxy will use the firewall instance to obtain both request and response objects which will be fed down the filter chain, so it is also possible to use this functionality to control the functionality of the response. When the request has passed through the security filter chain, the reset method will be called. With the default implementation this means that the original values of servletPath and pathInfo will be returned thereafter, instead of the modified ones used for security pattern matching.

FilterChainProxy 将使用防火墙实例来获取请求和响应对象,这些对象将被反馈到过滤器链中,因此也可以使用此功能来控制响应的功能。当请求通过安全过滤器链时,将调用 reset 方法。对于默认实现,这意味着之后将返回 servletPath 和 pathInfo 的原始值,而不是用于安全模式匹配的修改后的值。

---

过滤器生命周期Note the Filter lifecycle mismatch between the servlet container and IoC container. As described in the DelegatingFilterProxy Javadocs, we recommend you allow the IoC container to manage the lifecycle instead of the servlet container. FilterChainProxy does not invoke the standard filter lifecycle methods on any filter beans that you add to the application context.

注意 servlet 容器和 IoC 容器之间的过滤器生命周期不匹配。如 DelegatingFilterProxy Javadocs 中所述,我们建议您允许 IoC 容器而不是 servlet 容器来管理生命周期。 FilterChainProxy 不会在您添加到应用程序上下文的任何过滤器 bean 上调用标准过滤器生命周期方法。

FilterChainProxy 继承 Spring Web 框架的 GenericFilterBean(Spring 对 Filter 接口的基本实现),从而可以使用 Spring 进行管理。

这里我们主要关注的还是内部配置的 List<SecurityFilterChain>, 其中 SecurityFilterChain 为最高层接口,其实现类为 DefaultSecurityFilterChain。

DefaultSecurityFilterChain 内部包括 RequestMatcher 和 List<Filter>。

RequestMatcher 用于匹配 HttpServletRequest,确定策略实现的规则是否与提供的请求匹配,并可以返回 MatchResult。

首先可以看出 FitlerChainProxy 可以代理一条或多条过滤器链,每条过滤器链都包含配置的多个过滤器和请求匹配器。

同时,由于 FilterChainProxy 父类的缘故,需要实现 doFilter() 方法。此方法包括三个入参:

  1. ServletRequest: 请求报文
  2. ServletResponse: 响应报文
  3. FilterChain: 过滤器链中的下一个 Filter

doFilter()

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	// 从请求报文的属性中获取 FILTER_APPLIED
	boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
	// 获取到属性,调用内置方法 doFilterInternal()
	if (!clearContext) {
		doFilterInternal(request, response, chain);
		return;
	}
	// 未能获取对应参数
	try {
		// 设置属性,执行内置方法
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		doFilterInternal(request, response, chain);
	}
	catch (RequestRejectedException ex) {
		this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
	}
	finally {
		// 显式清除上下文
		SecurityContextHolder.clearContext();
		request.removeAttribute(FILTER_APPLIED);
	}
}
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();
		// 下一个 FilterChain 调用 doFilter 方法
		chain.doFilter(firewallRequest, firewallResponse);
		return;
	}
	if (logger.isDebugEnabled()) {
		logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
	}
	// 存在防火墙请求对应的过滤器链,构建虚拟过滤器链
	VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
	// 调用虚拟过滤器链的 doFilter 方法(在 originalChainA 和 originalChainB 之间接入虚拟过滤器链)
	virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}

内部类 VirtualFilterChain

/**
 * Internal {@code FilterChain} implementation that is used to pass a request through
 * the additional internal list of filters which match the request.
 */
private static final class VirtualFilterChain implements FilterChain {

	private final FilterChain originalChain;

	private final List<Filter> additionalFilters;

	private final FirewalledRequest firewalledRequest;

	private final int size;

	private int currentPosition = 0;

	private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain,
			List<Filter> additionalFilters) {
		this.originalChain = chain;
		this.additionalFilters = additionalFilters;
		this.size = additionalFilters.size();
		this.firewalledRequest = firewalledRequest;
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
		if (this.currentPosition == this.size) {
			if (logger.isDebugEnabled()) {
				logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
			}
			// Deactivate path stripping as we exit the security filter chain
			this.firewalledRequest.reset();
			this.originalChain.doFilter(request, response);
			return;
		}
		this.currentPosition++;
		Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
		if (logger.isTraceEnabled()) {
			logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(),
					this.currentPosition, this.size));
		}
		nextFilter.doFilter(request, response, this);
	}

}