欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Spring Security 如何执行权限验证

最编程 2024-10-19 07:09:11
...

阅读本文之前,请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大!

FilterSecurityInterceptor

FilterSecurityInterceptor 是负责权限验证的过滤器。一般来说,权限验证是一系列业务逻辑处理完成以后,最后需要解决的问题。所以默认情况下 security 会把和权限有关的过滤器,放在 VirtualFilter chains 的最后一位。

FilterSecurityInterceptor.doFilter 方法定义了两个权限验证逻辑。分别是 super.beforeInvocation 方法代表的权限前置验证与 super.afterInvocation 代表的后置权限验证。

public void invoke(FilterInvocation fi) throws IOException, ServletException {
	if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
		fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
	} else {
				// first time this request being called, so perform security checking
		if (fi.getRequest() != null && observeOncePerRequest) {
			fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}

		InterceptorStatusToken token = super.beforeInvocation(fi);

		try {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}

		super.afterInvocation(token, null);
	}
}

和身份验证的设计类似,前置权限验证的业务处理也是参考模板方法设计模式的理念,将核心处理流程定义在父类(super) AbstractSecurityInterceptor 中的。

InterceptorStatusToken token = super.beforeInvocation(fi);

再通过关联 AccessDecisionManager 的方式,把身份验证通过的认证对象委托给 AccessDecisionManager 执行。

Authentication authenticated = authenticateIfRequired();
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;
}

		// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
	attributes);

if (runAs == null) {
	if (debug) {
		logger.debug("RunAsManager did not change Authentication object");
	}

			// no further work post-invocation
	return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
		attributes, object);
}
else {
	if (debug) {
		logger.debug("Switching to RunAs Authentication: " + runAs);
	}

	SecurityContext origCtx = SecurityContextHolder.getContext();
	SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
	SecurityContextHolder.getContext().setAuthentication(runAs);

			// need to revert to token.Authenticated post-invocation
	return new InterceptorStatusToken(origCtx, true, attributes, object);
}

在前置权限校验执行完毕后,会使用 this.runAsManager.buildRunAs 与 SecurityContextHolder.getContext().setAuthentication(runAs) 方法将当前上下文中的身份认证成功的对象备份,然后重新拷贝一个新的鉴权成功的对象到上下文的中。


		// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
	attributes);

	SecurityContext origCtx = SecurityContextHolder.getContext();
	SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
	SecurityContextHolder.getContext().setAuthentication(runAs);
	

这个处理初看会有点难以理解,但是仔细思考一下,这其实是一个友好且细腻的操作。

原因是 FilterSecurityInterceptor 在大部分情况下是最后一个默认过滤器,如果说还有后续过滤操作的话,那这些肯定是你的自定义的过滤器对象行为产生的。安全起见,security 将权限认证过后的 authenticated 对象拷贝了一份副本,方便用户在后续的自定义过滤器中使用。

即使自定义过滤器中不小心修改了全局上下文中的 authenticate 对象也无妨,因为 super.finallyInvocation 方法

finally {
	super.finallyInvocation(token);
}

会保证自定义过滤器全部执行完毕以后,再把 copy source 「归还」到上下文中。这样,在后续的业务处理中就不用担心获取到一个不安全地 Authentication 对象了。

PrePostAnnotationSecurityMetadataSource.getAttributes

在 security 中使用的几种鉴权注解一共有以下四种:

  • PreFilter
  • PreAuthorize
  • PostFilter
  • PostAuthorize

其中最常用的是 PreAuthorize,它的含义是在执行目标方法前执行鉴权逻辑。前置过滤器执行的第一个逻辑,就是使用 getAttributes 方法获取目标方法上的注解。

public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
		if (method.getDeclaringClass() == Object.class) {
			return Collections.emptyList();
		}

		logger.trace("Looking for Pre/Post annotations for method '" + method.getName()
				+ "' on target class '" + targetClass + "'");
		PreFilter preFilter = findAnnotation(method, targetClass, PreFilter.class);
		PreAuthorize preAuthorize = findAnnotation(method, targetClass,
				PreAuthorize.class);
		PostFilter postFilter = findAnnotation(method, targetClass, PostFilter.class);
		// TODO: Can we check for void methods and throw an exception here?
		PostAuthorize postAuthorize = findAnnotation(method, targetClass,
				PostAuthorize.class);

		if (preFilter == null && preAuthorize == null && postFilter == null
				&& postAuthorize == null) {
			// There is no meta-data so return
			logger.trace("No expression annotations found");
			return Collections.emptyList();
		}

		String preFilterAttribute = preFilter == null ? null : preFilter.value();
		String filterObject = preFilter == null ? null : preFilter.filterTarget();
		String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value();
		String postFilterAttribute = postFilter == null ? null : postFilter.value();
		String postAuthorizeAttribute = postAuthorize == null ? null : postAuthorize
				.value();

		ArrayList<ConfigAttribute> attrs = new ArrayList<>(2);

		PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(
				preFilterAttribute, filterObject, preAuthorizeAttribute);

		if (pre != null) {
			attrs.add(pre);
		}

		PostInvocationAttribute post = attributeFactory.createPostInvocationAttribute(
				postFilterAttribute, postAuthorizeAttribute);

		if (post != null) {
			attrs.add(post);
		}

		attrs.trimToSize();

		return attrs;
	}

当通过 getAttributes 获取到对应的鉴权注解后,通过 createPreInvocationAttribute 将注解的 value——一般是 el 表达式,解析成 Express 对象后封装起来等待后续取用。

String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value();
PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(
				preFilterAttribute, filterObject, preAuthorizeAttribute);


public PreInvocationAttribute createPreInvocationAttribute(String preFilterAttribute,
			String filterObject, String preAuthorizeAttribute) {
		try {
			// TODO: Optimization of permitAll
			ExpressionParser parser = getParser();
			Expression preAuthorizeExpression = preAuthorizeAttribute == null ? parser
					.parseExpression("permitAll") : parser
					.parseExpression(preAuthorizeAttribute);
			Expression preFilterExpression = preFilterAttribute == null ? null : parser
					.parseExpression(preFilterAttribute);
			return new PreInvocationExpressionAttribute(preFilterExpression,
					filterObject, preAuthorizeExpression);
		}
		catch (ParseException e) {
			throw new IllegalArgumentException("Failed to parse expression '"
					+ e.getExpressionString() + "'", e);
		}
	}

AccessDecisionManager.decide -> AffirmativeBased.decide

说完了 getAttribute,让我们回到 beforeInvocation 方法的后续处理。你可能已经猜到了,和身份认证一样 filter 是不会实现实际的权限验证逻辑的。权限验证被交给了过滤器关联的 AccessDecisionManage.decide 方法来决定。

AccessDecisionManager 是一个接口,它的实现一共有 3 个类。AbstractAccessDecisionManager 是实现接口的抽象类,提供模板方法的定义。AffirmativeBased、ConsensusBased、UnanimousBased 是三个继承 AbstractAccessDecisionManager 的子类实现,分别代表了三种不同的记分策略。

以 AffirmativeBased 作为代表来说明的话,主要内容就是把 authentication 与封装的鉴权注解 el 表达式传递给关联的成员 decisionVoters 的 vote 方法进行处理。

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();
	}

PreInvocationAuthorizationAdviceVoter.vote

和上面的架构设计一样, AccessDecisionVoter 的实现 PreInvocationAuthorizationAdviceVoter.vote 看起来像是要开始处理真正意义上的权限问题了,但是其实依然没到最核心的部分。
vote 方法会返回一个 preAdvice.before 的结果,这个结果会被转换成 ACCESS_GRANTED : ACCESS_DENIED 这两个数字。为什么 preAdvice.before 的调用不直接返回 boolean 类型的鉴权结果?想想之前的 AffirmativeBased.decide 方法的设计思路:根据多个不同的 voter 返回的结果,我们可以使用不同的投票策略来对分数进行判定。返回一个数字,而不是 boolean 更便于计算最终得分。

public int vote(Authentication authentication, MethodInvocation method,
			Collection<ConfigAttribute> attributes) {

		// Find prefilter and preauth (or combined) attributes
		// if both null, abstain
		// else call advice with them

		PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);

		if (preAttr == null) {
			// No expression based metadata, so abstain
			return ACCESS_ABSTAIN;
		}

		boolean allowed = preAdvice.before(authentication, method, preAttr);

		return allowed ? ACCESS_GRANTED : ACCESS_DENIED;
	}

ExpressionBasedPreInvocationAdvice.before

这次我向你保证,ExpressionBasedPreInvocationAdvice.before 真的是专门负责判定权限校验是否通过的方法了。由于之前已经封装好了 el 表达式的 Express 对象,所以调用 ExpressionUtils.evaluateAsBoolean 就可以直接根据表达式与执行上下文 ctx -> WebSecurityExpressionRoot extends SecurityExpressionRoot 使用 getValue() 方法获取执行结果——即权限校验的结果。


public boolean before(Authentication authentication, MethodInvocation mi,
			PreInvocationAttribute attr) {
		PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr;
		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				mi);
		Expression preFilter = preAttr.getFilterExpression();
		Expression preAuthorize = preAttr.getAuthorizeExpression();

		if (preFilter != null) {
			Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi);

			expressionHandler.filter(filterTarget, preFilter, ctx);
		}

		if (preAuthorize == null) {
			return true;
		}

		return ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx);
	}

举一反三一下,既然鉴权逻辑就是一个运行时的 el 表达式解析后的结果,那利用 el 表达式可以调用类方法的特性,是不是自定义一个专门的表达式处理程序就可以随心所欲地返回鉴权结果了呢?

自定义 EL 表达式

    /**
     * sysdomain + edit permission
     */
    public static final String SYSDOMAIN_AND_EDITPERMISSION = SYSDOMAIN + " && " + EDIT_PERMISSION;

    public static final String EDIT_PERMISSION_MENU = "@authorizeAppService.hasPermission('EDIT_PERMISSION_MENU')";

自定义 EL 表达式处理方法


public class AuthorizeAppService implements AuthorizeAppInterface {

    @Override
    public boolean hasDomain(String domain) throws IllegalArgumentException {
        if (StringUtils.isEmpty(domain)) {
            throw new IllegalArgumentException("hasDomain accepted a invalid express parameter");
        }
        AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        return StringUtils.equals(domain, user.getDomain().getCode());
    }

    @Override
    public boolean hasRole(String... role) throws IllegalArgumentException {
        if (ArrayUtils.isEmpty(role)) {
            throw new IllegalArgumentException("hasRole accepted a invalid express parameter\"");
        }

        AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return user.getAuthRoles().stream().map(AuthRole::getCode).collect(Collectors.toList()).containsAll(CollectionUtils.arrayToList(role));
    }

    @Override
    public boolean hasPermission(String... permission) throws IllegalArgumentException {
        if (ArrayUtils.isEmpty(permission)) {
            throw new IllegalArgumentException("hasPermission accepted a invalid express parameter\"");
        }
        AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        Set<String> permissions = user.getAllpermissions().stream().map(AuthPermission::getCode).collect(Collectors.toSet());
        return permissions.containsAll(CollectionUtils.arrayToList(permission));
    }
}

最后,我们来探讨一下 security 为什么要费尽周折,通过一长串的聚合和依赖把权限校验这个处理流程传递这么长的路径来实现?为什么不在一开始就是 make el Express and return el.value() 搞定所有的事情?

我想这是因为 security 是一个 architect first 的设计产物。架构优先的设计,高扩展性都特别的重要。这一系列的聚合和依赖,都是「用组合解决问题」思想的体现。尽量用组合来解决问题会带来了很多好处,比如通过 GlobalMethodSecurityConfiguration 类进行的高度可配置化。


@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalMethodSecurityConfiguration
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware

{
	@Bean
	public MethodSecurityMetadataSource methodSecurityMetadataSource() {
		List<MethodSecurityMetadataSource> sources = new ArrayList<>();
		ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
			getExpressionHandler());
		MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
		if (customMethodSecurityMetadataSource != null) {
			sources.add(customMethodSecurityMetadataSource);
		}

		boolean hasCustom = customMethodSecurityMetadataSource != null;
		boolean isPre