1. 程式人生 > 程式設計 >給spring-security提了一個issue

給spring-security提了一個issue

前言

今天在用spring-security的角色繼承時,遇到了一個坑,通過除錯原始碼解決了,然後發現這應該是spring-security本身的一個小問題,然後就在Spring官方的GitHub上提了一個issue

正文

我在使用spring-security的角色繼承,關鍵程式碼片段如下:

...
// 定義角色繼承,兩個角色繼承之間用空格或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原始碼的註釋也可看到相關說明

/**
 * 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啊,就是這樣就可以啊。然後試了改了改許可權表示式,結果還是不行,然後我想了想,除錯原始碼吧,除錯原始碼一般都是必殺技。在除錯原始碼的過程中,我逐漸發現了問題所在。

我先給出角色表示式解析以及角色繼承解析的相關程式碼路徑,大家可按這個路徑跟蹤。 角色表示式解析:

// 由上到下為執行路徑,最上端是入口點
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#hasRole

{%asset_img 2.png%}

執行到下圖中這裡後,返回的是false也就是授權未通過,沒有對應角色,當前擁有的角色是從org.springframework.security.access.hierarchicalroles.RoleHierarchy#getReachableGrantedAuthorities獲得的,裡面並沒有需要的角色"ROLE",因此自然就是false

{%asset_img 3.png%}

那麼為什麼沒有呢,按照角色繼承的定義,應該能夠有才對啊,這是我們就需要看角色繼承表示式生成具體角色的邏輯了,這個邏輯的程式碼路徑是這個:

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_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);

}
複製程式碼