聊聊spring security的role hierarchy

829 阅读4分钟

本文就来研究一下spring security的role hierarchy

背景

默认情况下,userDetailsService建立的用户,他们的权限是没有继承关系的

	@Bean
    @Override
    protected UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("demoUser1").password("123456")
                .authorities("ROLE_USER","read_x").build());
        manager.createUser(User.withUsername("admin").password("123456")
                .authorities("ROLE_ADMIN").build());
        return manager;
    }

比如这两个

	@GetMapping("/admin")
    @Secured("ROLE_ADMIN")
    public String admin(){
        return "admin";
    }

    @GetMapping("/user")
    @Secured("ROLE_USER")
    public String user(){
        return "user";
    }

admin登录只能访问/admin,访问不了/user;而user登录只能访问/user

这通常不大符合我们的业务需求,一般admin拥有所有权限的,也就是它应该能访问/user。这个问题扩展开来就是角色权限的继承问题,role hierarchy

RoleHierarchy

spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/access/hierarchicalroles/RoleHierarchy.java

            ROLE_ADMIN > ROLE_STAFF
			ROLE_STAFF > ROLE_USER
			ROLE_USER > ROLE_GUEST

spring security提供了RoleHierarchy,可以让你去定义各类角色的层级关系

config

@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
@Configuration
public class RoleConfig extends GlobalMethodSecurityConfiguration{

    @Override
    protected AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
        ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
        expressionAdvice.setExpressionHandler(getExpressionHandler());
//        if (prePostEnabled()) {
            decisionVoters
                    .add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
//        }
//        if (jsr250Enabled()) {
            decisionVoters.add(new Jsr250Voter());
//        }
//        decisionVoters.add(new RoleVoter());
        decisionVoters.add(roleHierarchyVoter());
        decisionVoters.add(new AuthenticatedVoter());

        return new AffirmativeBased(decisionVoters);
    }


    @Bean
    public RoleHierarchyVoter roleHierarchyVoter() {
        return new RoleHierarchyVoter(roleHierarchy());
    }

    @Bean
    public RoleHierarchy roleHierarchy(){
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy(
                "ROLE_ADMIN > ROLE_USER\n"+
                        " ROLE_USER > ROLE_ANONYMOUS\n"
        );
        return roleHierarchy;
    }
}

这里通过重写GlobalMethodSecurityConfiguration的accessDecisionManager方法,给decisionVoters添加roleHierarchyVoter 默认是使用RoleVoter,它不支持继承关系,这里替换为roleHierarchyVoter 这样就大功告成了,admin也可以访问user权限的页面/接口

RoleHierarchyVoter

spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/access/vote/RoleHierarchyVoter.java

/**
 * Extended RoleVoter which uses a {@link RoleHierarchy} definition to determine the roles
 * allocated to the current user before voting.
 *
 * @author Luke Taylor
 * @since 2.0.4
 */
public class RoleHierarchyVoter extends RoleVoter {
	private RoleHierarchy roleHierarchy = null;

	public RoleHierarchyVoter(RoleHierarchy roleHierarchy) {
		Assert.notNull(roleHierarchy, "RoleHierarchy must not be null");
		this.roleHierarchy = roleHierarchy;
	}

	/**
	 * Calls the <tt>RoleHierarchy</tt> to obtain the complete set of user authorities.
	 */
	@Override
	Collection<? extends GrantedAuthority> extractAuthorities(
			Authentication authentication) {
		return roleHierarchy.getReachableGrantedAuthorities(authentication
				.getAuthorities());
	}
}

这类继承了RoleVoter,重写了extractAuthorities,使用roleHierarchy去获取grantedAuthorities

继承关系的构建

spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java

	/**
	 * Set the role hierarchy and pre-calculate for every role the set of all reachable
	 * roles, i.e. all roles lower in the hierarchy of every given role. Pre-calculation
	 * is done for performance reasons (reachable roles can then be calculated in O(1)
	 * time). During pre-calculation, cycles in role hierarchy are detected and will cause
	 * a <tt>CycleInRoleHierarchyException</tt> to be thrown.
	 *
	 * @param roleHierarchyStringRepresentation - String definition of the role hierarchy.
	 */
	public void setHierarchy(String roleHierarchyStringRepresentation) {
		this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;

		logger.debug("setHierarchy() - The following role hierarchy was set: "
				+ roleHierarchyStringRepresentation);

		buildRolesReachableInOneStepMap();
		buildRolesReachableInOneOrMoreStepsMap();
	}

设置层级关系之后,通过buildRolesReachableInOneStepMap以及buildRolesReachableInOneOrMoreStepsMap这两个方法去构建映射

buildRolesReachableInOneStepMap

	/**
	 * rolesReachableInOneStepMap is a Map that under the key of a specific role name
	 * contains a set of all roles reachable from this role in 1 step
	 */
	private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;

	/**
	 * 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.");
		}
	}

	private void addReachableRoles(Set<GrantedAuthority> reachableRoles,
			GrantedAuthority authority) {

		for (GrantedAuthority testAuthority : reachableRoles) {
			String testKey = testAuthority.getAuthority();
			if ((testKey != null) && (testKey.equals(authority.getAuthority()))) {
				return;
			}
		}
		reachableRoles.add(authority);
	}

Map<GrantedAuthority, Set> rolesReachableInOneStepMap ,这里将第一级的直连关系存到这个map当中

假设级联关系是

A > B
B > C
C > D
D > E
D > F

那么这个map就类似于

A --> [B]
B --> [C]
C --> [D]
D --> [E,F]

buildRolesReachableInOneOrMoreStepsMap

	/**
	 * rolesReachableInOneOrMoreStepsMap is a Map that under the key of a specific role
	 * name contains a set of all roles reachable from this role in 1 or more steps
	 */
	private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;

	/**
	 * For every higher role from rolesReachableInOneStepMap store all roles that are
	 * reachable from it in the map of roles reachable in one or more steps. (Or throw a
	 * CycleInRoleHierarchyException if a cycle in the role hierarchy definition is
	 * detected)
	 */
	private void buildRolesReachableInOneOrMoreStepsMap() {
		this.rolesReachableInOneOrMoreStepsMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>();
		// iterate over all higher roles from rolesReachableInOneStepMap

		for (GrantedAuthority role : this.rolesReachableInOneStepMap.keySet()) {
			Set<GrantedAuthority> rolesToVisitSet = new HashSet<GrantedAuthority>();

			if (this.rolesReachableInOneStepMap.containsKey(role)) {
				rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(role));
			}

			Set<GrantedAuthority> visitedRolesSet = new HashSet<GrantedAuthority>();

			while (!rolesToVisitSet.isEmpty()) {
				// take a role from the rolesToVisit set
				GrantedAuthority aRole = rolesToVisitSet.iterator().next();
				rolesToVisitSet.remove(aRole);
				addReachableRoles(visitedRolesSet, aRole);
				if (this.rolesReachableInOneStepMap.containsKey(aRole)) {
					Set<GrantedAuthority> newReachableRoles = this.rolesReachableInOneStepMap
							.get(aRole);

					// definition of a cycle: you can reach the role you are starting from
					if (rolesToVisitSet.contains(role)
							|| visitedRolesSet.contains(role)) {
						throw new CycleInRoleHierarchyException();
					}
					else {
						// no cycle
						rolesToVisitSet.addAll(newReachableRoles);
					}
				}
			}
			this.rolesReachableInOneOrMoreStepsMap.put(role, visitedRolesSet);

			logger.debug("buildRolesReachableInOneOrMoreStepsMap() - From role " + role
					+ " one can reach " + visitedRolesSet + " in one or more steps.");
		}

	}

Map<GrantedAuthority, Set> rolesReachableInOneOrMoreStepsMap这个将间接的层级关系拉平 这里实际用了递归来完成层级的所有级联关系映射,rolesToVisitSet不断remove和有条件地add,递归终止条件是rolesToVisitSet为empty

一级的map如下

A --> [B]
B --> [C]
C --> [D]
D --> [E,F]

构造完之后如下

A --> [B,C,D,E,F]
B --> [C,D,E,F]
C --> [D,E,F]
D --> [E,F]

RoleHierarchyImpl

spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java

public Collection<GrantedAuthority> getReachableGrantedAuthorities(
			Collection<? extends GrantedAuthority> authorities) {
		if (authorities == null || authorities.isEmpty()) {
			return AuthorityUtils.NO_AUTHORITIES;
		}

		Set<GrantedAuthority> reachableRoles = new HashSet<GrantedAuthority>();

		for (GrantedAuthority authority : authorities) {
			addReachableRoles(reachableRoles, authority);
			Set<GrantedAuthority> additionalReachableRoles = getRolesReachableInOneOrMoreSteps(
					authority);
			if (additionalReachableRoles != null) {
				reachableRoles.addAll(additionalReachableRoles);
			}
		}

		if (logger.isDebugEnabled()) {
			logger.debug("getReachableGrantedAuthorities() - From the roles "
					+ authorities + " one can reach " + reachableRoles
					+ " in zero or more steps.");
		}

		List<GrantedAuthority> reachableRoleList = new ArrayList<GrantedAuthority>(
				reachableRoles.size());
		reachableRoleList.addAll(reachableRoles);

		return reachableRoleList;
	}
	// SEC-863
	private Set<GrantedAuthority> getRolesReachableInOneOrMoreSteps(
			GrantedAuthority authority) {

		if (authority.getAuthority() == null) {
			return null;
		}

		for (GrantedAuthority testAuthority : this.rolesReachableInOneOrMoreStepsMap
				.keySet()) {
			String testKey = testAuthority.getAuthority();
			if ((testKey != null) && (testKey.equals(authority.getAuthority()))) {
				return this.rolesReachableInOneOrMoreStepsMap.get(testAuthority);
			}
		}

		return null;
	}

getReachableGrantedAuthorities方法通过之前构造好的rolesReachableInOneOrMoreStepsMap来获取所有级联层级关系

这样就大功告成了

doc