Role Hierarchy를 구현해보자!

김재연·2025년 2월 20일
8
post-thumbnail

배경

Spring Security

저희 회사는 다양한 기능에 다양한 권한을 부여하기 위해 Spring Security를 사용하고 있습니다.

이렇게 Spring Security로 권한 관리를 잘 하고 있던 와중, 저희에게는 다음과 같은 니즈가 생기게 되었습니다.

API 별로 권한을 따로 관리 하고 싶다. 하지만, 그러면 권한이 너무 많아지고 관리하기가 힘들다.

저희에게는 위와 같은 니즈가 새로 생기게 되었고 이를 위해 Spring Security의 Role Hierarchy 기능을 사용하기로 결정하였습니다.

RoleHierarchy를 알아보자

Spring Security의 RoleHierarchy는 도대체 무엇일까요?

RoleHierarchy interface 입니다.

설명을 읽어보면서 대충 해당 메서드가 어떤 역할을 하는지 알아봅시다.

Returns an array of all reachable authorities.

Reachable authorities are the directly assigned authorities plus all authorities that are (transitively) reachable from them in the role hierarchy.

Example: Role hierarchy: ROLE_A > ROLE_B > ROLE_C. Directly assigned authority: ROLE_A. Reachable authorities: ROLE_A, ROLE_B, ROLE_C.

Params: authorities – - List of the directly assigned authorities.

Returns: List of all reachable authorities given the assigned authorities.

해당 Doc을 요약하면, 도달 가능한 권한은 직접 부여된 권한과, 플러스로 권한 계층에서 다이렉트로 부여된 권한이 도달 가능한 권한 목록을 반환하는 메서드라고 합니다.

Example로는 권한 계층이 만약에 ROLE_A -> ROLE_B -> ROLE_C (제일 하위 계층) 이와 같이 이루어져 있고, 직접 부여된 권한이 ROLE_A라면, 도달 가능한 권한은 직접 부여된 권한과, 도달 가능한 권한들인 ROLE_A, ROLE_B, ROLE_C인 것입니다.

만일 여기서, 직접 부여된 권한이 ROLE_B라면 가능한 권한은 ROLE_B, ROLE_C일 것입니다.

추가적으로, 이런 식으로 Role Hierarchy를 사용하여 도달 가능한 권한 목록을 반환하면 도대체 뭐가 어떻게 동작하는 것이라는 궁금증이 들 것입니다.

이는 바로 다음과 같이 동작합니다.

계층이 이와 같이 있다고 한다면

이와 같이 동작하게 되는 것입니다.

정리하면, 정의된 계층 구조 안에서 Direct로 부여된 권한들에서 도달 가능한 모든 권한들을 추출하고 이를 기반으로 인증을 수행하는 것입니다.

기본적인 구현

RoleHierarchy를 구현하는 가장 쉬운 방법은 다음과 같습니다.

기본적으로 제공하는 RoleHierachy 구현체에 위와 같은 String을 넘기는 방법입니다.

위에서 다루었듯이, ROLE_A > ROLE_B 라 함은, ROLE_BROLE_A의 하위 계층임을 의미합니다. (도달 가능한 권한)

그래서, 여러 라인으로 각각 ROLE들의 계층 구조를 표현하여 fromHierarchy (정적 팩토리 메서드)에 넘기면 알아서 String에 표현되어 있는 모든 권한 각각 -> 도달 가능한 권한 목록을 나타내는 트리를 내부적으로 생성해서 가지고 있습니다.

이 상태에서 권한이 필요한 경우가 생기면 getReachableGrantedAuthorities를 호출하여 유저가 가지고 있는 권한 목록을 활용해 도달 가능한 모든 권한 목록을 반환합니다.

구현체를 조금만 까보자

정말 간단하게 구현체를 까보면 다음과 같습니다. (추후 이 구조를 활용합니다)

전체적인 구조는 fromHierarchy를 호출하면 buildRolesReachableInOneStepMap -결과-> buildRolesReachableInOneOrMoreStepsMap 를 거쳐 RoleHierarchy를 완성합니다.

그러면 이 두 개의 메서드만 보면 트리가 어떻게 생성되는지 알 수 있겠죠?

  • buildRolesReachableInOneStepMap

JavaDoc에 적혀있듯이, 해당 정점에서 한번만 움직이면 도달 가능한 정점까지만 추출해냅니다. (코드에 대한 설명은 주석으로 남겨놓았습니다.)

예를 들어 다음과 같은 String이 있다고 했을 때

ROLE_A > ROLE_B > ROLE_C
ROLE_D > ROLE_E

아래와 같이 트리가 형성되는 것입니다.

  • buildRolesReachableInOneOrMoreStepsMap

해당 메서드는 한번이 아닌 더 움직여서 도달가능한 모든 권한들을 정점에 매핑해주는 역할을 해줍니다. (자세한 코드에 대한 설명은 주석으로 남겨놓았습니다.)

그러면 이전 그래프에서 다음과 같이 변경되게 되는 것입니다.

이제 이렇게 되면 ROLE 이름만 주어지면, 도달 가능한 모든 권한들을 한번에 얻을 수 있겠죠?

  • getReachableGrantAuthroties (직접 부여된 권한과 도달 가능한 권한 목록 반환)

해당 메서드는 그냥 위 그래프를 활용해서, 들어온 유저가 직접 보유한 권한 목록에서, 직접 보유한 권한을 포함한 도달 가능한 모든 권한 목록을 반환합니다. (로직에 대한 설명은 주석으로 작성해놓았습니다.)

기존 구현체를 이용하는 게 아닌, 직접 구현을 진행해보자!

하지만 기본적으로 제공된 구현체를 사용하는 것은 굉장히 유연하지 않은 경우가 존재할 수 있습니다.

그러니, 기존 구현체를 사용하기 보다는 직접 구현을 진행하는 방향으로 진행했습니다.

권한 계층 정의

일단, 쉽게 권한 계층을 컨트롤 하기 위해서는 DB를 활용해야합니다.

그렇기 때문에, 다음과 같이 RoleLayer라는 Entity 및 Domain을 만들어 DB를 통해 계층을 관리할 수 있도록 합니다.

@Builder
@Getter
public class RoleLayer { // Entity는 생략하도록 하겠습니다.
  
    private Long id;  
    private String parentRoleName;  
    private String roleName;  
	@Override  
	public boolean equals(Object o) {  
	    if (o == null || getClass() != o.getClass()) {  
	        return false;  
	    }  
	    RoleLayer roleLayer = (RoleLayer) o;  
	    return Objects.equals(id, roleLayer.id);  
	}  
	  
	@Override  
	public int hashCode() {  
	    return Objects.hash(id);  
	}
  
}
구현

이렇게 RoleLayer를 만들었으니, 이를 가지고 있는 RoleLayers라는 일급컬렉션을 만들어, RoleLayer의 리스트를 넣기만 하더라도, 트리를 생성할 수 있도록 합니다.

지금부터 순차적으로 이에 대해 설명하도록 하겠습니다.

public static RoleLayers from(List<RoleLayer> values) {  
    validateDuplicateRole(values); // RoleLayer 목록에 대한 검증
    Map<String, Set<RoleLayer>> layerTree = makeTreeFrom(values); // oneStep Tree 구성
    Map<String, Set<RoleLayer>> resultOfLayers = new HashMap<>();
    values.forEach(roleLayer -> fillOutLayersByTree(roleLayer, roleLayer, layerTree, resultOfLayers)); // oneStepOrMore Tree 구성
  
    return new RoleLayers(resultOfLayers);  
}  
  
private static void validateDuplicateRole(List<RoleLayer> values) { // ROLE NAME 중 겹치는 것이 존재하면 예외를 발생시킵니다.
    long count = values.stream()  
            .map(RoleLayer::getRoleName)  
            .distinct()  
            .count();  
  
    if (count == values.size()) {  
        return;  
    }  
  
    throw new InternalException("Duplicate role name");  
}
  • oneStep Tree 구성
private static Map<String, Set<RoleLayer>> makeTreeFrom(List<RoleLayer> values) {  
    Map<String, Set<RoleLayer>> layerTree = new HashMap<>();  
    values.forEach(roleLayer -> fillOutTree(roleLayer, layerTree));  
  
    return layerTree;  
}  
  
private static void fillOutTree(RoleLayer roleLayer, Map<String, Set<RoleLayer>> layerTree) {  
    String parentRoleName = roleLayer.getParentRoleName();  
    String childRoleName = roleLayer.getRoleName();  
  
    if (Objects.isNull(parentRoleName) || Objects.isNull(childRoleName)) {  
        return;  
    }  
  
    layerTree.computeIfAbsent(parentRoleName, ignored -> new HashSet<>());  
    layerTree.get(parentRoleName).add(roleLayer); // parentRoleName -> childRoleLayer
}

굉장히 간단하죠? 모든 roleLayer를 돌면서, parentRoleLayer -> childRoleLayer 간선을 생성해줍니다.

그러니까 이와 같은 구조를 만든다고 보시면 됩니다.

  • OneOrMoreStep Tree 구성
private static void fillOutLayersByTree(  
        RoleLayer rootRoleLayer, // 시작 RoleLayer
        RoleLayer currentRoleLayer, // 현재 순회중인 RoleLayer
        Map<String, Set<RoleLayer>> layersTree, // One Step Tree
        Map<String, Set<RoleLayer>> resultOfLayers // 우리가 만들고자 하는 트리
) {  
    resultOfLayers.computeIfAbsent(rootRoleLayer.getRoleName(), ignored -> new HashSet<>()); // rootRoleLayer가 resultOfLayers에 Key로 존재하지 않으면 생성
    boolean isExistsCyclicHierarchical = !resultOfLayers.get(rootRoleLayer.getRoleName())  
            .add(currentRoleLayer); // 시작 RoleLayer의 도달 가능한 RoleLayer를 등록하는 과정에서 중복으로 방문하는 경우 사이클로 간주합니다.
  
    if (isExistsCyclicHierarchical) {
        throw new InternalException("A circular hierarchical reference exists");  
    }  
  
    Set<RoleLayer> directlyLowerRoleLayers = layersTree.get(currentRoleLayer.getRoleName()); // 직접 도달 가능한 권한 목록 추출
  
    if (Objects.isNull(directlyLowerRoleLayers)) {  
        return;  
    }  
  
    directlyLowerRoleLayers.forEach( // 재귀적으로 순회
            lowerRoleLayer -> fillOutLayersByTree(  
                    rootRoleLayer, lowerRoleLayer, layersTree, resultOfLayers  
            )  
    );  
}

이렇게해서 resultOfLayers를 완성시키면, 다음과 같이 트리를 구성할 수 있게 되는 것입니다.

도달 가능한 권한 목록 반환받기
public List<GrantedAuthority> getPossibleRoleLayers(List<? extends GrantedAuthority> authorities) {  
    if (authorities == null || authorities.isEmpty()) {  
        return AuthorityUtils.NO_AUTHORITIES;  
    }  
  
    return authorities.stream()  
            .distinct()  
            .map(this::getPossibleLowerLayers) // 도달 가능한 하위 계층 Role 구하기
            .flatMap(List::stream)  
            .map(this::addRolePrefix) // ROLE_ 붙이기
            .distinct() // 중복 제거
            .toList();  
}  
  
private List<GrantedAuthority> getPossibleLowerLayers(GrantedAuthority authority) {  
    if (Objects.isNull(authority.getAuthority())) {  
        return List.of(authority);  
    }  
  
    return getReachedAllRoles(authority);  
}  
  
private List<GrantedAuthority> getReachedAllRoles(GrantedAuthority authority) {  
    GrantedAuthority removedRolePrefixAuthority = new SimpleGrantedAuthority(removeRolePrefix(authority)); // ROLE_ 제거한 authority
    Set<GrantedAuthority> reachableRoles = layers.getOrDefault(  
                    removedRolePrefixAuthority.getAuthority(),  
                    Collections.emptySet()  
            ) // 바로 reachable한 Role 목록 반환
            .stream()  
            .map(RoleLayer::getRoleName)  
            .map(SimpleGrantedAuthority::new)  
            .collect(Collectors.toSet()); // 중복 제거
  
    reachableRoles.add(removedRolePrefixAuthority); // 직접 부여된 권한 추가
  
    return new ArrayList<>(reachableRoles);  
}

생성했던 resultOfLayers를 활용해 사용자에게 직접 부여된 권한 목록에서 도달 가능한 권한 목록들을 추출해서 반환합니다.

빈으로 등록하자.

이렇게 다 구현을 했으면 이제 Bean으로 등록해야겠죠?

@Configuration  
@RequiredArgsConstructor  
@EnableMethodSecurity  
public class SecurityConfiguration {  

	... Security 관련 다른 설정들

    @Bean  
    @RefreshScope    
    public RoleHierarchy roleHierarchy() { // Bean 정의
        List<RoleLayer> roleLayers = roleLayerRepository.findAll();  
        RoleLayers roleLayerCollections = RoleLayers.from(roleLayers);  
  
        return RoleHierarchyImpl.from(roleLayerCollections);  
    }  
  
}
@AllArgsConstructor  
public class RoleHierarchyImpl implements RoleHierarchy {  
  
    private final RoleLayers roleLayers;  
  
    public static RoleHierarchyImpl from(RoleLayers roleLayers) {  
        return new RoleHierarchyImpl(roleLayers);  
    }  
  
    @Override  
    public Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(  
            Collection<? extends GrantedAuthority> authorities  
    ) {  
        return roleLayers.getPossibleRoleLayers((List<? extends GrantedAuthority>) authorities);  
    }  
}

이렇게 등록하게 되면 이제 사용할 준비는 모두 완료가 되었습니다.

테스트 코드 및 실제 디버깅으로 잘 동작하는지 확인하기

그러면 이제 실제 동작을 확인할 때가 왔습니다.

테스트 코드는 다음과 같고, 사용한 그래프는 다음과 같습니다.

class RoleLayersTest {  
  
    /**  
     * https://i.imgur.com/OS8OXYC.png 해당 트리는 다음과 같습니다.  
     */    
     private static final RoleLayers roleLayers = RoleLayers.from(  
            List.of(  
                    RoleLayer.builder()  
                            .roleName("1")  
                            .build()  
                    ,  
                    RoleLayerFixture.createWithSetColumns( // 사내에서 사용하고 있는 TextFixture 생성 방식입니다. (지정한 값 이외에는 랜덤 값 배치)
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "1"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "2")  
                    ),  
                    RoleLayerFixture.createWithSetColumns(  
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "1"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "3")  
                    ),  
                    RoleLayerFixture.createWithSetColumns(  
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "3"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "4")  
                    ),  
                    RoleLayerFixture.createWithSetColumns(  
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "3"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "5")  
                    ),  
                    RoleLayerFixture.createWithSetColumns(  
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "5"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "6")  
                    ),  
                    RoleLayerFixture.createWithSetColumns(  
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "2"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "7")  
                    ),  
                    RoleLayerFixture.createWithSetColumns(  
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "2"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "8")  
                    ),  
                    RoleLayerFixture.createWithSetColumns(  
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "7"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "9")  
                    ),  
                    RoleLayerFixture.createWithSetColumns(  
                            Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "1"),  
                            Pair.of(RoleLayerColumn.ROLE_NAME, "10")  
                    )  
            )  
    );  
  
    @ParameterizedTest  
    @MethodSource("expectedRoles")  
    void 입력된_RoleLayer에_포함된_하위_계층까지_전부_가져옵니다(String authority, List<Integer> expected) {  
        // given  
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = List.of(  
                new SimpleGrantedAuthority(authority));  
  
        // when  
        List<GrantedAuthority> actual = roleLayers.getPossibleRoleLayers(simpleGrantedAuthorities);  
  
        // then  
        assertThat(actual).hasSize(expected.size());  
        assertThat(actual).extracting(GrantedAuthority::getAuthority)  
                .isEqualTo(expected);  
    }  
  
    private static Stream<Arguments> expectedRoles() {  
        return Stream.of(  
                Arguments.of("ROLE_1",  
                        List.of("ROLE_1", "ROLE_2", "ROLE_3", "ROLE_4", "ROLE_5", "ROLE_6", "ROLE_7",  
                                "ROLE_8", "ROLE_9", "ROLE_10")),  
                Arguments.of("ROLE_2", List.of("ROLE_2", "ROLE_7", "ROLE_8", "ROLE_9")),  
                Arguments.of("ROLE_3", List.of("ROLE_3", "ROLE_4", "ROLE_5", "ROLE_6")),  
                Arguments.of("ROLE_4", List.of("ROLE_4")),  
                Arguments.of("ROLE_5", List.of("ROLE_5", "ROLE_6")),  
                Arguments.of("ROLE_6", List.of("ROLE_6")),  
                Arguments.of("ROLE_7", List.of("ROLE_7", "ROLE_9")),  
                Arguments.of("ROLE_8", List.of("ROLE_8")),  
                Arguments.of("ROLE_9", List.of("ROLE_9")),  
                Arguments.of("ROLE_10", List.of("ROLE_10"))  
        );  
    }  
  
    @Test  
    void 가능한_RoleLayer들의_목록을_한번에_반환합니다() {  
        // given  
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = List.of(  
                new SimpleGrantedAuthority("ROLE_3"),  
                new SimpleGrantedAuthority("ROLE_5"),  
                new SimpleGrantedAuthority("ROLE_7")  
        );  
  
        // when  
        List<GrantedAuthority> actual = roleLayers.getPossibleRoleLayers(simpleGrantedAuthorities);  
  
        // then  
        assertThat(actual).extracting(GrantedAuthority::getAuthority)  
                .hasSize(6)  
                .isEqualTo(List.of("ROLE_3", "ROLE_4", "ROLE_5", "ROLE_6", "ROLE_7", "ROLE_9"));  
    }  
  
    @Test  
    void 중복된_Role이_존재하면_예외가_발생합니다() {  
        // given  
        List<RoleLayer> duplicateRoleLayers = List.of(RoleLayerFixture.createWithSetColumns(  
                        Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "1"),  
                        Pair.of(RoleLayerColumn.ROLE_NAME, "2")  
                ),  
                RoleLayerFixture.createWithSetColumns(  
                        Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "1"),  
                        Pair.of(RoleLayerColumn.ROLE_NAME, "2")  
                ));  
  
        // when  
        ThrowingCallable actual = () -> RoleLayers.from(duplicateRoleLayers);  
  
        // then  
        assertThatThrownBy(actual).isExactlyInstanceOf(InternalException.class)  
                .hasMessage("Duplicate role name");  
    }  
  
    @Test  
    void 순환_참조가_존재하면_예외가_발생합니다() {  
        // given  
        List<RoleLayer> duplicateRoleLayers = List.of(RoleLayerFixture.createWithSetColumns(  
                        Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "4"),  
                        Pair.of(RoleLayerColumn.ROLE_NAME, "2")  
                ),  
                RoleLayerFixture.createWithSetColumns(  
                        Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "2"),  
                        Pair.of(RoleLayerColumn.ROLE_NAME, "3")  
                ),  
                RoleLayerFixture.createWithSetColumns(  
                        Pair.of(RoleLayerColumn.PARENT_ROLE_NAME, "3"),  
                        Pair.of(RoleLayerColumn.ROLE_NAME, "4")  
                )  
        );  
  
        // when  
        ThrowingCallable actual = () -> RoleLayers.from(duplicateRoleLayers);  
  
        // then  
        assertThatThrownBy(actual).isExactlyInstanceOf(InternalException.class)  
                .hasMessage("A circular hierarchical reference exists");  
    }  
  
}

잘 동작하는 것을 확인할 수 있습니다. 그러면 이제 마지막으로 실제로도 동작하는지를 확인해야겠죠?

사실, 실제 유저에게 권한을 부여해가며 테스트를 할 수는 없는 상황이라서, Debuging mode의 기능에서 실제로 변수를 활용하여 동작을 시킬 수 있는 기능을 사용했습니다.

그래서, 하위 권한으로 총 28개를 가지고 있는 권한을 넣어 getReachableGrantAuthroties 실행해본 결과 result로 29개(직접 부여된 권한 포함)의 권한을 반환하는 것을 볼 수 있습니다.

성공적으로 구현되었다는 것이죠!

결과

주요 코드 모음
@Configuration  
@RequiredArgsConstructor  
@EnableMethodSecurity  
public class SecurityConfiguration {  

	... Security 관련 다른 설정들

    @Bean  
    @RefreshScope    
    public RoleHierarchy roleHierarchy() { // Bean 정의
        List<RoleLayer> roleLayers = roleLayerRepository.findAll();  
        RoleLayers roleLayerCollections = RoleLayers.from(roleLayers);  
  
        return RoleHierarchyImpl.from(roleLayerCollections);  
    }  
  
}
@AllArgsConstructor  
public class RoleHierarchyImpl implements RoleHierarchy {  
  
    private final RoleLayers roleLayers;  
  
    public static RoleHierarchyImpl from(RoleLayers roleLayers) {  
        return new RoleHierarchyImpl(roleLayers);  
    }  
  
    @Override  
    public Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(  
            Collection<? extends GrantedAuthority> authorities  
    ) {  
        return roleLayers.getPossibleRoleLayers((List<? extends GrantedAuthority>) authorities);  
    }  
  
}
@AllArgsConstructor  
public class RoleLayers {  
  
    private final Map<String, Set<RoleLayer>> layers;  
    private final String ROLE_PREFIX = "ROLE_";  
  
    public static RoleLayers from(List<RoleLayer> values) {  
        validateDuplicateRole(values);  
        Map<String, Set<RoleLayer>> layerTree = makeTreeFrom(values);  
        Map<String, Set<RoleLayer>> resultOfLayers = new HashMap<>();  
        values.forEach(roleLayer -> fillOutLayersByTree(roleLayer, roleLayer, layerTree, resultOfLayers));  
  
        return new RoleLayers(resultOfLayers);  
    }  
  
    private static void validateDuplicateRole(List<RoleLayer> values) {  
        long count = values.stream()  
                .map(RoleLayer::getRoleName)  
                .distinct()  
                .count();  
  
        if (count == values.size()) {  
            return;  
        }  
  
        throw new InternalException("Duplicate role name");  
    }  
  
    private static Map<String, Set<RoleLayer>> makeTreeFrom(List<RoleLayer> values) {  
        Map<String, Set<RoleLayer>> layerTree = new HashMap<>();  
        values.forEach(roleLayer -> fillOutTree(roleLayer, layerTree));  
  
        return layerTree;  
    }  
  
    private static void fillOutTree(RoleLayer roleLayer, Map<String, Set<RoleLayer>> layerTree) {  
        String parentRoleName = roleLayer.getParentRoleName();  
        String childRoleName = roleLayer.getRoleName();  
  
        if (Objects.isNull(parentRoleName) || Objects.isNull(childRoleName)) {  
            return;  
        }  
  
        layerTree.computeIfAbsent(parentRoleName, ignored -> new HashSet<>());  
        layerTree.get(parentRoleName).add(roleLayer);  
    }  
  
    private static void fillOutLayersByTree(  
            RoleLayer rootRoleLayer,  
            RoleLayer currentRoleLayer,  
            Map<String, Set<RoleLayer>> layersTree,  
            Map<String, Set<RoleLayer>> resultOfLayers  
    ) {  
        resultOfLayers.computeIfAbsent(rootRoleLayer.getRoleName(), ignored -> new HashSet<>());  
        boolean isExistsCyclicHierarchical = !resultOfLayers.get(rootRoleLayer.getRoleName())  
                .add(currentRoleLayer);  
  
        if (isExistsCyclicHierarchical) {  
            throw new InternalException("A circular hierarchical reference exists");  
        }  
  
        Set<RoleLayer> directlyLowerRoleLayers = layersTree.get(currentRoleLayer.getRoleName());  
  
        if (Objects.isNull(directlyLowerRoleLayers)) {  
            return;  
        }  
  
        directlyLowerRoleLayers.forEach(  
                lowerRoleLayer -> fillOutLayersByTree(  
                        rootRoleLayer, lowerRoleLayer, layersTree, resultOfLayers  
                )  
        );  
    }  
  
    public List<GrantedAuthority> getPossibleRoleLayers(List<? extends GrantedAuthority> authorities) {  
        if (authorities == null || authorities.isEmpty()) {  
            return AuthorityUtils.NO_AUTHORITIES;  
        }  
  
        return authorities.stream()  
                .distinct()  
                .map(this::getPossibleLowerLayers)  
                .flatMap(List::stream)  
                .map(this::addRolePrefix)  
                .distinct()  
                .toList();  
    }  
  
    private List<GrantedAuthority> getPossibleLowerLayers(GrantedAuthority authority) {  
        if (Objects.isNull(authority.getAuthority())) {  
            return List.of(authority);  
        }  
  
        return getReachedAllRoles(authority);  
    }  
  
    private List<GrantedAuthority> getReachedAllRoles(GrantedAuthority authority) {  
        GrantedAuthority removedRolePrefixAuthority = new SimpleGrantedAuthority(removeRolePrefix(authority));  
        Set<GrantedAuthority> reachableRoles = layers.getOrDefault(  
                        removedRolePrefixAuthority.getAuthority(),  
                        Collections.emptySet()  
                )  
                .stream()  
                .map(RoleLayer::getRoleName)  
                .map(SimpleGrantedAuthority::new)  
                .collect(Collectors.toSet());  
  
        reachableRoles.add(removedRolePrefixAuthority);  
  
        return new ArrayList<>(reachableRoles);  
    }  
  
    private String removeRolePrefix(GrantedAuthority authority) {  
        return authority.getAuthority()  
                .substring(ROLE_PREFIX.length());  
    }  
  
    private GrantedAuthority addRolePrefix(GrantedAuthority authority) {  
        String authorityNameWithRolePrefix = ROLE_PREFIX + authority.getAuthority();  
        return new SimpleGrantedAuthority(authorityNameWithRolePrefix);  
    }  
  
}
@Builder
@Getter
public class RoleLayer {
  
    private Long id;  
    private String parentRoleName;  
    private String roleName;  

	@Override  
	public boolean equals(Object o) {  
	    if (o == null || getClass() != o.getClass()) {  
	        return false;  
	    }  
	    RoleLayer roleLayer = (RoleLayer) o;  
	    return Objects.equals(id, roleLayer.id);  
	}  
	  
	@Override  
	public int hashCode() {  
	    return Objects.hash(id);  
	}
  
}
얻어낸 결과


RoleHierarchy Implementation에 쓰여져 있는 설명인데, 굉장히 자랑스러워 하는 것 같아 인상깊어서 스샷을 찍어놓았습니다 ㅋㅋㅋ

이렇게 RoleHierarch를 통해, 우리 회사 역시도 관리해야 하는 권한은 굉장히 많아졌지만, 상황에 맞게 상위 계층에 위치한 권한을 유저에게 부여해, 위 사진에 쓰여있는 것과 같이 access rule을 굉장히 간단하고 우아하게 만들 수 있는 기반이 되었습니다.

마무리

긴 글 읽어주셔서 감사합니다.

profile
끊임없이 '성장'하는 개발자 김재연입니다.

2개의 댓글

comment-user-thumbnail
2025년 3월 4일

omg

1개의 답글

관련 채용 정보