给spring-security提了一个issue

前言

今天在用spring-security的角色继承时,遇到了一个坑,经过调试源码解决了,而后发现这应该是spring-security自己的一个小问题,而后就在Spring官方的GitHub上提了一个issuejava

正文

我在使用spring-security的角色继承,关键代码片断以下:git

...
// 定义角色继承,两个角色继承之间用空格或and链接
roleHierarchyImpl.setHierarchy("ROLE_DEVELOPER > ROLE_SUPERVISOR and ROLE_SUPERVISOR > ROLE_ADMIN and ROLE_ADMIN > ROLE_USER and ROLE_USER > ROLE_ANONYMOUS");
...
复制代码
...
// 定义须要的权限表达式
.access("hasRole('USER')")
...
复制代码

上边关于角色继承的定义方式,是我在使用以前版本的spring-security得到经验,同时,经过spring-security源码的注释也可看到相关说明github

/** * The simple interface of a role hierarchy. * * @author Michael Mayr */
public interface RoleHierarchy {

   /** * Returns an array of all reachable authorities. * <p> * Reachable authorities are the directly assigned authorities plus all authorities * that are (transitively) reachable from them in the role hierarchy. * <p> * Example:<br> * Role hierarchy: ROLE_A &gt; ROLE_B and ROLE_B &gt; ROLE_C.<br> * Directly assigned authority: ROLE_A.<br> * Reachable authorities: ROLE_A, ROLE_B, ROLE_C. * * @param authorities - List of the directly assigned authorities. * @return List of all reachable authorities given the assigned authorities. */
   public Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
         Collection<? extends GrantedAuthority> authorities);

}
复制代码

可是,当我实际跑起来后,发现根本不行,角色继承没生效。我就很纳闷了,原来用过spring-security啊,就是这样就能够啊。而后试了改了改权限表达式,结果仍是不行,而后我想了想,调试源码吧,调试源码通常都是必杀技。在调试源码的过程当中,我逐渐发现了问题所在。web

我先给出角色表达式解析以及角色继承解析的相关代码路径,你们可按这个路径跟踪。 角色表达式解析:spring

// 由上到下为执行路径,最上端是入口点
org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter

org.springframework.security.web.access.intercept.FilterSecurityInterceptor#doFilter

org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation

org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke

org.springframework.security.access.AccessDecisionManager#decide

org.springframework.security.access.vote.AffirmativeBased#decide

org.springframework.security.web.access.expression.WebExpressionVoter#vote

org.springframework.security.access.expression.ExpressionUtils#evaluateAsBoolean

org.springframework.expression.spel.standard.SpelExpression#getValue(org.springframework.expression.EvaluationContext, java.lang.Class<T>)

org.springframework.expression.spel.ast.SpelNodeImpl#getTypedValue

org.springframework.expression.spel.ast.MethodReference#getValueInternal(org.springframework.expression.spel.ExpressionState)

org.springframework.expression.spel.ast.MethodReference#getCachedExecutor

org.springframework.expression.spel.support.ReflectiveMethodExecutor#execute

java.lang.reflect.Method#invoke

org.springframework.security.access.expression.SecurityExpressionRoot#hasRole

org.springframework.security.access.expression.SecurityExpressionRoot#hasAnyRole

org.springframework.security.access.expression.SecurityExpressionRoot#hasAnyAuthorityName

复制代码

注意上边执行路径中的 java.lang.reflect.Method#invoke 实际上,权限控制表达式内部的原理是是用反射去执行对应的用于判断是否有权限方法的,也就是执行 org.springframework.security.access.expression.SecurityExpressionRoot#hasRoleexpress

{%asset_img 2.png%}bash

执行到下图中这里后,返回的是false也就是受权未经过,没有对应角色,当前拥有的角色是从org.springframework.security.access.hierarchicalroles.RoleHierarchy#getReachableGrantedAuthorities得到的,里面并无须要的角色"ROLE",所以天然就是falseide

{%asset_img 3.png%}ui

那么为何没有呢,按照角色继承的定义,应该可以有才对啊,这是咱们就须要看角色继承表达式生成具体角色的逻辑了,这个逻辑的代码路径是这个:this

org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl#setHierarchy

org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl#buildRolesReachableInOneStepMap

org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl#buildRolesReachableInOneOrMoreStepsMap
复制代码

经过跟踪这些代码,咱们从中能够看出,实际上,正确的角色继承表达式应该这样定义:

...
roleHierarchyImpl.setHierarchy("ROLE_DEVELOPER > ROLE_SUPERVISOR > ROLE_ADMIN > ROLE_USER > ROLE_ANONYMOUS");
...
复制代码

实际上定义角色继承表达式的规则已经变了,然而,在spring-security代码库中的RoleHierarchy这个类的注释,还保留着旧版的角色继承表达式的定义方式的说明,这应当是代码更新了可是注释未更新,我按照以往的经验以及注释的说明去写,结果掉坑里了。

总结

经过此次问题的排查,能够说明:必要的注释能够有,可是不要过度依赖于注释,要相信代码自己,此外在此次调试源码的过程当中我还发现了一个调试源码的技巧:利用Drop Frame,能够倒推代码的执行路径。

20190613更新

后来我发现,在4.2.x的spring-security中,角色继承表达式不单单能够用"and"链接符,它用任何一种链接符均可以。如下为我在issue page上与@rwinch的对话

Thanks for the clarification. RoleHiearchy isn't implementation specific. Instead it is trying to convey information rather than the configuration. That said, I can see how it might lead to confusion. Can you think of a way that makes the Javadoc in RoleHiearchy read better? If you do have a better wording, would you be willing to open a PR to change the RoleHiearchy Javadoc?

Or, to put it another way, perhaps the current version of "RoleHierarchyImpl" is not compatible with the definition rules of the old version of the "role inheritance" expression.

Can you clarify why you believe RoleHierarchyImpl worked differently and when it did? The code has gone largely untouched for over 10 years.

I reviewed my previous code these two days and learned that I used the version 4.2.2 before. Then, I looked at the source code for 4.2.2 and found out why the "and" connector can be used in the "role inheritance" expression in this version. In fact, in this version, the connector can be any string. Let's look at the code for details:

/** * Parse input and build the map for the roles reachable in one step: the higher role * will become a key that references a set of the reachable lower roles. */
	private void buildRolesReachableInOneStepMap() {
		Pattern pattern = Pattern.compile("(\\s*([^\\s>]+)\\s*>\\s*([^\\s>]+))");

		Matcher roleHierarchyMatcher = pattern
				.matcher(this.roleHierarchyStringRepresentation);
		this.rolesReachableInOneStepMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>();

		while (roleHierarchyMatcher.find()) {
			GrantedAuthority higherRole = new SimpleGrantedAuthority(
					roleHierarchyMatcher.group(2));
			GrantedAuthority lowerRole = new SimpleGrantedAuthority(
					roleHierarchyMatcher.group(3));
			Set<GrantedAuthority> rolesReachableInOneStepSet;

			if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
				rolesReachableInOneStepSet = new HashSet<GrantedAuthority>();
				this.rolesReachableInOneStepMap.put(higherRole,
						rolesReachableInOneStepSet);
			}
			else {
				rolesReachableInOneStepSet = this.rolesReachableInOneStepMap
						.get(higherRole);
			}
			addReachableRoles(rolesReachableInOneStepSet, lowerRole);

			logger.debug("buildRolesReachableInOneStepMap() - From role " + higherRole
					+ " one can reach role " + lowerRole + " in one step.");
		}
	}
复制代码

In this code, a regular expression grouping match is used to find a group that matches the rule. In fact, the string to be matched can contain any kind of connector. Any kind of connector will not affect the result of the expression "roleHierarchyMatcher.find()" equal to true.

@Test
	public void testRegexForRoleHierarchyString() {
		Pattern pattern = Pattern.compile("(\\s*([^\\s>]+)\\s*>\\s*([^\\s>]+))");

		String roleHierarchyStringSplitByAnd = "ROLE_HIGHEST > ROLE_HIGHER and ROLE_HIGHER > ROLE_LOW and ROLE_LOW > ROLE_LOWER";
		String roleHierarchyStringSplitByOr = "ROLE_HIGHEST > ROLE_HIGHER or ROLE_HIGHER > ROLE_LOW or ROLE_LOW > ROLE_LOWER";
		String roleHierarchyStringSplitBySpace = "ROLE_HIGHEST > ROLE_HIGHER ROLE_HIGHER > ROLE_LOW ROLE_LOW > ROLE_LOWER";
		String roleHierarchyStringSplitByWhatever = "ROLE_HIGHEST > ROLE_HIGHER xxx ROLE_HIGHER > ROLE_LOW xxx ROLE_LOW > ROLE_LOWER";

		List<String> roleHierarchyStrings = new LinkedList<String>();
		roleHierarchyStrings.add(roleHierarchyStringSplitByAnd);
		roleHierarchyStrings.add(roleHierarchyStringSplitByOr);
		roleHierarchyStrings.add(roleHierarchyStringSplitBySpace);
		roleHierarchyStrings.add(roleHierarchyStringSplitByWhatever);

		for (String roleHierarchyString : roleHierarchyStrings) {
			Matcher roleHierarchyMatcher = pattern.matcher(roleHierarchyString);
			if (!roleHierarchyMatcher.find()) {
				throw new RuntimeException("I'm dead. X﹏X");
			}
		}
		System.out.println("All pass");
	}
复制代码
All pass

Process finished with exit code 0
复制代码

That is to say, in the version 4.2.2 or the adjacent version, the "RoleHierarchy" comment is neither an error nor a correct one, XD.

/** * The simple interface of a role hierarchy. * * @author Michael Mayr */
public interface RoleHierarchy {

	/** * Returns an array of all reachable authorities. * <p> * Reachable authorities are the directly assigned authorities plus all authorities * that are (transitively) reachable from them in the role hierarchy. * <p> * Example:<br> * Role hierarchy: ROLE_A &gt; ROLE_B and ROLE_B &gt; ROLE_C.<br> * Directly assigned authority: ROLE_A.<br> * Reachable authorities: ROLE_A, ROLE_B, ROLE_C. * * @param authorities - List of the directly assigned authorities. * @return List of all reachable authorities given the assigned authorities. */
	public Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
			Collection<? extends GrantedAuthority> authorities);

}
复制代码