张帆 | 作者
今天在用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 > ROLE_B and ROLE_B > 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
执行到下图中这里后,返回的是 false
也就是授权未通过,没有对应角色,当前拥有的角色是从 org.springframework.security.access.hierarchicalroles.RoleHierarchy#getReachableGrantedAuthorities
获得的,里面并没有需要的角色”ROLE”,因此自然就是 false
那 么为什么没有呢,按照角色继承的定义,应该能够有才对啊,这是我们就需要看角色继承表达式生成具体角色的逻辑了,这个逻辑的代码路径是这个:
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
,可以倒推代码的执行路径。
后来我发现,在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."); } }
I n 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 > ROLE_B and ROLE_B > 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); }
近期热文
1 |
基于支付场景下的微服务改造与性能优化 |
2 |
MySQL单表数据不要超过500万行: 是经验数值,还是黄金铁律? |
3 |
人人都是 API 设计者: 我对 RESTful API、GraphQL、RPC API 的思考 |
新书出版
→
期待您的加入,一起成长
>>> 加入「后端圈」大家庭 (点击加入)<<<