2023-06-12  阅读(3)
原文作者:发飙的蜗牛咻咻咻~ 原文地址:https://blog.csdn.net/qq_36221788/category_11009647.html

〇、前文回顾

在实战篇《手把手教你如何使用Spring Security(下):访问控制》我们学习了Spring Security强大的访问控制能力,只需要进行寥寥几行的配置就能做到权限的控制,本篇来看看它到底是如何做到的。


一、再聊过滤器链

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

  • 所以,一般情况下最后一个过滤器是做权限访问控制的核心过滤器FilterSecurityInterceptor ,而倒数第二个是异常翻译过滤器ExceptionTranslationFilter ,将异常进行翻译然后响应给客户端。

  • 比如我们实战项目过滤器链图解

    202306122224579291.png


二、过滤器的创建

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 限制访问的方式。

    202306122224584452.png

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() 方法,这里定义了很多默认的过滤器配置,其中就包括当前过滤器配置。

    202306122224593893.png


三、源码流程

FilterSecurityInterceptor

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

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

202306122225008194.png 方法源码如下,继续往下走

    	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')

202306122225017185.png
所以它实际调用的是 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(Stringrole)
hasAnyRole(“BUYER”,“SELLER”) 用于拥有任意一个角色权限时返回true hasAnyRole(‘ROLE_BUYER’,‘ROLE_BUYER’) hasAnyRole(String…roles)
hasAuthority(“BUYER”) 同hasRole hasAuthority(‘ROLE_BUYER’) hasAuthority(Stringrole)
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(StringipAddress),该方法在WebSecurityExpressionRoot类中
access("@rbacService.hasPermission(request,authentication)") 可以自定义Spel表达式 @rbacService.hasPermission(request,authentication) hasPermission(request,authentication),该方法在自定义的RbacServiceImpl类中

四、总结

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

五、系列文章

Spring Security 系列

Spring Security OAuth 系列


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文