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

发布时间:2021-07-05 18:40 来源:脚本之家 阅读:0 作者:木兮同学 栏目: 开发技术

目录

    〇、前文回顾

    在实战篇《》我们学习了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')
    方法源码如下,继续往下走

    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) 方法,方法里面主要将异常信息和错误码设置到响应头,然后响应到客户端,请求结束。

    补充:权限表达式

    四、总结

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

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

    免责声明:本站发布的内容(图片、视频和文字)以原创、来自互联网转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系QQ:712375056 进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。