聊聊spring security的role hierarchy

weixin_34321977發表於2017-12-22

本文就來研究一下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<GrantedAuthority>> 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<GrantedAuthority>> 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

相關文章