Spring Security源码解析之权限访问控制是如何做到的

 更新时间:2021年05月17日 10:36:38   作者:木兮同学  
Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,下面小编给大家介绍Spring Security源码解析之权限访问控制是如何做到的,感兴趣的朋友跟随小编一起看看吧

〇、前文回顾

在实战篇《话说Spring Security权限管理(源码详解)》我们学习了Spring Security强大的访问控制能力,只需要进行寥寥几行的配置就能做到权限的控制,本篇来看看它到底是如何做到的。


一、再聊过滤器链

源码篇中反复提到,请求进来需要经过的是一堆过滤器形成的过滤器链,走完过滤器链未抛出异常则可以继续访问后台接口资源,而最后一个过滤器就是来判断请求是否有权限继续访问后台资源,如果没有则会将拒绝访问的异常往上向异常过滤器抛,异常过滤器会对异常进行翻译,然后响应给客户端。

所以,一般情况下最后一个过滤器是做权限访问控制的核心过滤器FilterSecurityInterceptor ,而倒数第二个是异常翻译过滤器ExceptionTranslationFilter ,将异常进行翻译然后响应给客户端。比如我们实战项目过滤器链图解

过滤器


二、过滤器的创建

FilterSecurityInterceptor的创建

这个过滤器的配置器是 ExpressionUrlAuthorizationConfigurer ,它的父类 AbstractInterceptUrlConfigurer 中的 configure() 方法创建了这个过滤器。

abstract class AbstractInterceptUrlConfigurer<C extends AbstractInterceptUrlConfigurer<C, H>, H extends HttpSecurityBuilder<H>>
		extends AbstractHttpConfigurer<C, H> {
	...
	@Override
	public void configure(H http) throws Exception {
		FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
		if (metadataSource == null) {
			return;
		}
		FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
				http, metadataSource, http.getSharedObject(AuthenticationManager.class));
		if (filterSecurityInterceptorOncePerRequest != null) {
			securityInterceptor
					.setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
		}
		securityInterceptor = postProcess(securityInterceptor);
		http.addFilter(securityInterceptor);
		http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
	}
	...
}

这个过滤器的配置器是在 HttpSecurityauthorizeRequests() 方法中apply进来的,在我们自己配置的核心配置器中使用的就是该种基于 HttpServletRequest 限制访问的方式。

核心配置器

ExceptionTranslationFilter的创建

这个过滤器的配置器是 ExceptionHandlingConfigurer ,它自己的 configure() 方法中创建了这个过滤器。

public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>> extends
		AbstractHttpConfigurer<ExceptionHandlingConfigurer<H>, H> {
	...
	@Override
	public void configure(H http) throws Exception {
		AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
		ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
				entryPoint, getRequestCache(http));
		if (accessDeniedHandler != null) {
			exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler);
		}
		exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
		http.addFilter(exceptionTranslationFilter);
	}
	...
}

这个过滤器的配置器是在 HttpSecurityexceptionHandling() 方法中apply进来的,和上面不同的是,这个过滤器配置器会默认被apply进 HttpSecurity,在 WebSecurityConfigurerAdapter 中的 init() 方法,里面调用了 getHttp() 方法,这里定义了很多默认的过滤器配置,其中就包括当前过滤器配置。

核心配置器


三、源码流程

FilterSecurityInterceptor

  • 进入:doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  • 进入:invoke(FilterInvocation fi)
  • 进入:beforeInvocation(Object object)

这个方法里面有个 attributes ,里面获取的就是当前request请求所能匹配中的权限Spel表达式,比如这里是 hasRole('ROLE_BUYER')
Spel表达式 方法源码如下,继续往下走

protected InterceptorStatusToken beforeInvocation(Object object) {
		
		...

		// 获取当前request请求所能匹配中的权限Spel表达式
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);
				
		...

		// Attempt authorization
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}
		
		...
		
	}

进入:decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)

这里有个投票器,投票结果为1表示可以访问直接返回,投票结果为-1表示拒绝访问,向上抛拒绝访问异常,这里使用的投票器是 WebExpressionVoter

public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;

		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;

			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break;
			}
		}

		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}

进入:vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes)

这里面其实就是使用Spring的Spel表达式进行投票,使用请求中的权限表达式组装Expression,使用Token令牌中的权限组装EvaluationContext,然后调用 ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx)

public int vote(Authentication authentication, FilterInvocation fi,
			Collection<ConfigAttribute> attributes) {
		assert authentication != null;
		assert fi != null;
		assert attributes != null;

		WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

		if (weca == null) {
			return ACCESS_ABSTAIN;
		}

		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				fi);
		ctx = weca.postProcess(ctx, fi);

		return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
				: ACCESS_DENIED;
	}

evaluateAsBoolean() 方法里面就是调用Expression的 getValue() 方法,获取实际的匹配结果,如下图Spel表达式为 hasRole('ROLE_BUYER')
在这里插入图片描述
所以它实际调用的是 SecurityExpressionRoot#hasRole 方法(关于权限表达式对应实际调用的方法,在《手把手教你如何使用Spring Security(下):访问控制》文章中已贴出,下面文章也补充一份),里面的逻辑其实就是判断Token令牌中是否包含有 ROLE_BUYER 的角色,有的话返回true,否则返回false,如下为 SecurityExpressionRoot#hasRole 方法源码:

private boolean hasAnyAuthorityName(String prefix, String... roles) {
		Set<String> roleSet = getAuthoritySet();

		for (String role : roles) {
			String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
			if (roleSet.contains(defaultedRole)) {
				return true;
			}
		}

		return false;
	}
  • 如果投票成功,则会一直返回到 invoke() 方法,再执行后续过滤器,未抛异常表示该请求已经有访问权限了
  • 假如投票失败,在 decide() 方法中会向上抛拒绝访问异常,一直往上抛直到被处理,往上反向跟踪发现这个过滤器一直没有处理拒绝访问异常,那就继续往上个过滤器抛,就到了我们的异常翻译过滤器 ExceptionTranslationFilter

ExceptionTranslationFilter

该过滤器的 doFilter() 方法很简单,没有逻辑处理,只对后续过滤器抛出的异常进行处理,源码如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null) {
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}

			if (ase != null) {
				handleSpringSecurityException(request, response, chain, ase);
			}
			else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}

当抛出拒绝访问异常后,继续调用 handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) 方法,方法里面主要将异常信息和错误码设置到响应头,然后响应到客户端,请求结束。

补充:权限表达式

权限表达式(ExpressionUrlAuthorizationConfigurer) 说明 Spel表达式 Spel表达式实际执行方法(SecurityExpressionOperations)
permitAll() 表示允许所有,永远返回true permitAll permitAll()
denyAll() 表示拒绝所有,永远返回false denyAll denyAll()
anonymous() 当前用户是anonymous时返回true anonymous isAnonymous()
rememberMe() 当前用户是rememberMe用户时返回true rememberMe isRememberMe()
authenticated() 当前用户不是anonymous时返回true authenticated isAuthenticated()
fullyAuthenticated() 当前用户既不是anonymous也不是rememberMe用户时返回true fullyAuthenticated isFullyAuthenticated()
hasRole(“BUYER”) 用户拥有指定权限时返回true hasRole(‘ROLE_BUYER') hasRole(String role)
hasAnyRole(“BUYER”,“SELLER”) 用于拥有任意一个角色权限时返回true hasAnyRole (‘ROLE_BUYER',‘ROLE_BUYER') hasAnyRole(String… roles)
hasAuthority(“BUYER”) 同hasRole hasAuthority(‘ROLE_BUYER') hasAuthority(String role)
hasAnyAuthority(“BUYER”,“SELLER”) 同hasAnyRole hasAnyAuthority (‘ROLE_BUYER',‘ROLE_BUYER') hasAnyAuthority(String… authorities)
hasIpAddress(‘192.168.1.0/24') 请求发送的Ip匹配时返回true hasIpAddress(‘192.168.1.0/24') hasIpAddress(String ipAddress),该方法在WebSecurityExpressionRoot类中
access("@rbacService.hasPermission(request, authentication)") 可以自定义Spel表达式 @rbacService.hasPermission (request, authentication) hasPermission(request, authentication) ,该方法在自定义的RbacServiceImpl类中

四、总结

  • 访问控制的核心过滤器是 FilterSecurityInterceptor ,当然这个是可选的,我们完全也可以自定义一个过滤器去处理权限访问。
  • 处理访问异常处理的过滤器是 ExceptionTranslationFilter ,里面逻辑很简单,给response设置异常信息错误码,再返回给客户端。

以上就是Spring Security源码解析之权限访问控制是如何做到的的详细内容,更多关于Spring Security权限访问控制的资料请关注脚本之家其它相关文章!

相关文章

  • idea创建properties文件,解决乱码问题

    idea创建properties文件,解决乱码问题

    这篇文章主要介绍了idea创建properties文件,解决乱码问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • Java成员变量与局部变量(动力节点Java学院整理)

    Java成员变量与局部变量(动力节点Java学院整理)

    这篇文章主要介绍了Java成员变量与局部变量的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-04-04
  • java编程SpringSecurity入门原理及应用简介

    java编程SpringSecurity入门原理及应用简介

    Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案
    2021-09-09
  • Java利用Strategy模式实现堆排序

    Java利用Strategy模式实现堆排序

    策略设计模式(Strategy):可以整体的替换一个算法的实现部分,能够整体的替换算法,能让我们轻松地用不同方法解决同一个问题。本文将利用Strategy模式实现堆排序,感兴趣的可以学习一下
    2022-09-09
  • Spring Boot中Bean定义方调用方式解析

    Spring Boot中Bean定义方调用方式解析

    这篇文章主要介绍了Spring Boot中Bean定义方调用方式解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • 解决IDEA集成Docker插件后出现日志乱码的问题

    解决IDEA集成Docker插件后出现日志乱码的问题

    这篇文章主要介绍了解决IDEA集成Docker插件后出现日志乱码的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • java开发https请求ssl不受信任问题解决方法

    java开发https请求ssl不受信任问题解决方法

    这篇文章主要介绍了java开发https请求ssl不受信任问题解决方法,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • spring security获取用户信息的实现代码

    spring security获取用户信息的实现代码

    这篇文章主要介绍了spring security获取用户信息的实现代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • 使用springboot对外部静态资源文件的处理操作

    使用springboot对外部静态资源文件的处理操作

    这篇文章主要介绍了使用springboot对外部静态资源文件的处理操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • 浅谈SpringMVC国际化支持

    浅谈SpringMVC国际化支持

    这篇文章主要介绍了浅谈SpringMVC国际化支持,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-03-03

最新评论