JUINTINATION

Spring Security RoleHierarchy로 계층권한 설정하기 본문

Java Spring Boot

Spring Security RoleHierarchy로 계층권한 설정하기

DEOKJAE KWON 2024. 8. 12. 18:14
반응형

이전 Board Clone 프로젝트에 Spring Security를 적용한 로그인 기능 구현하기에서 각 사용자의 역할(Role)에 따라 접근할 수 있는 URL을 다르게 설정했었다. 아래는 이를 명시한 SecurityConfig.java의 filterChain 메서드의 내용이다.

 

Board Clone 프로젝트에 Spring Security를 활용한 로그인 기능 구현하기

프로젝트 소개기존의 코드로 배우는 스프링 부트 웹 프로젝트 Board Clone 프로젝트는 예전에 스프링 공부를 위해 ETRI에서 연구연수생으로 근무할 때 대여했던 코드로 배우는 스프링 부트 웹 프로

juintination.tistory.com

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/css/**", "/vendor/**", "/favicon.ico/**").permitAll()
                    .requestMatchers("/", "/login", "/loginProc", "/join", "/joinProc", "/checkUsername").permitAll()
                    .requestMatchers("/admin").hasRole("ADMIN")
                    .requestMatchers("/board/**").hasAnyRole("ADMIN", "USER")
                    .requestMatchers("/replies/**").hasAnyRole("ADMIN", "USER")
                    .anyRequest().authenticated()
            );

    // 중략..

    return http.build();
}

ROLE_ADMIN 역할을 가진 사용자는 ROLE_ADMIN과 ROLE_USER 역할이 필요한 모든 URL에 접근할 수 있지만, ROLE_USER 역할을 가진 사용자는 ROLE_ADMIN 역할이 필요한 "/admin"과 같은 URL에는 접근할 수 없다는 것을 알 수 있다. 스프링 시큐리티에서 제공하는 기본적인 인가 정책을 사용할 때 각각의 역할은 연관 관계를 가지지 않기 때문에 위와 같이 일일이 필요한 역할을 모두 명시해두어야 한다.

이처럼 역할이 별로 없을 경우 기존 방식 또한 가시성이 좋아서 나쁘지 않지만, 역할이 매우 많아질 경우 일일이 명시하기도 힘들 뿐더러 유지보수 측면에서도 좋지 않을 것이다. 이를 해결하기 위해 RoleHierarchy를 적용할 것이다.

 

Authorization Architecture :: Spring Security

It is a common requirement that a particular role in an application should automatically "include" other roles. For example, in an application which has the concept of an "admin" and a "user" role, you may want an admin to be able to do everything a normal

docs.spring.io

RoleHierarchy

RoleHierarchy는 스프링 시큐리티의 역할 간의 계층 구조를 정의하는 기능으로, 이를 통해 더 높은 권한을 가진 역할이 더 낮은 권한을 가진 역할의 권한을 상속받도록 설정할 수 있다. 예를 들어, RoleHierarchy를 통해 ROLE_ADMIN 역할은 ROLE_MANAGER 역할보다 높은 권한을 가지며, ROLE_MANAGER 역할은 ROLE_USER 역할보다 높은 권한을 갖는다는 설정을 진행할 수 있다.

이때 ROLE_USER, ROLE_MANAGER, ROLE_ADMIN 순으로 접근 권한이 높아지는 계층 구조를 가지므로, ROLE_ADMIN 역할을 가진 사용자는 ROLE_USER, ROLE_MANAGER 역할이 필요한 URL에 따로 설정을 하지 않아도 접근할 수 있게 된다.

RoleHierarchy를 통해 정의한 역할 계층은 스프링 시큐리티가 접근 제어를 수행할 때 자동으로 적용되며, 각 역할에 대한 권한을 재사용할 수 있어 중복된 설정을 피할 수 있다.

/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.access.hierarchicalroles;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;

/**
 * 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 &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.
	 */
	Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
			Collection<? extends GrantedAuthority> authorities);

}

Example을 보면 권한 계층을 ROLE_A > ROLE_B > ROLE_C 와 같이 표현하는 것을 알 수 있다. 이제 이러한 RoleHierarchy를 활용하여 코드를 수정해볼 것이다.

config.SecurityConfig.java

package org.zerock.board.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public RoleHierarchy roleHierarchy() {

        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();

        hierarchy.setHierarchy("ROLE_ADMIN > ROLE_MANAGER\n" +
                "ROLE_MANAGER > ROLE_USER");

        return hierarchy;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/css/**", "/vendor/**", "/favicon.ico/**").permitAll()
                        .requestMatchers("/", "/login", "/loginProc", "/join", "/joinProc", "/checkUsername").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .requestMatchers("/manager").hasRole("MANAGER")
                        .requestMatchers("/board/**").hasAnyRole("USER")
                        .requestMatchers("/replies/**").hasAnyRole("USER")
                        .anyRequest().authenticated()
                );

        // 중략..

        return http.build();
    }
}

위의 코드를 보면 기존의 SecurityConfig에 roleHierarchy()라는 새로운 메서드가 추가된 것을 볼 수 있다. RoleHierarchy를 통해 역할 계층을 정의하는 메서드로, ROLE_ADMIN은 ROLE_MANAGER보다 높은 권한을 가지며, ROLE_MANAGER는 ROLE_USER보다 높은 권한을 갖는다는 설정이다.
ROLE_ADMIN 역할을 가진 사용자는 ROLE_MANAGER와 ROLE_USER 역할이 필요한 모든 URL에 접근할 수 있지만, ROLE_USER 역할을 가진 사용자는 ROLE_MANAGER와 ROLE_ADMIN 역할이 필요한 URL에는 접근할 수 없다.

requestMatchers로 HTTP 요청에 대해 역할 관련 접근 권한을 설정한 부분을 보면 아래와 같다.

  • "/admin": ADMIN 역할의 사용자만 접근할 수 있게 설정하였지만 admin 페이지는 따로 제작하지 않았으며, 역할 관련 테스트를 진행할 때만 사용하였다.
  • "/manager": ADMIN 역할과 MANAGER 역할의 사용자만 접근할 수 있게 설정하였지만 manager 페이지는 따로 제작하지 않았으며, 역할 관련 테스트를 진행할 때만 사용하였다.
  • "/board/**", "/replies/**": ADMIN, MANAGER, USER 역할이 있는 사용자면 접근할 수 있다.

만약 USER보다 계층이 낮은 역할을 추가한다면, 해당 역할은 "/board/**", "/replies/**" URL에 접근할 수 없을 것이다.


결론

지금 진행중인 프로젝트의 로그인을 위해 작성중인 membership-api-server를 보면 Member의 역할을 List 타입으로 일일이 추가하는 방식임을 확인할 수 있다. 그래서 위에서 언급한 유지보수 관련 문제와 코드의 복잡성 등의 문제가 있는데, RoleHierarchy를 적용해서 List 타입이 아닌 String 타입으로 역할을 지정하도록 수정할 수 있을 것이다.

기존의 코드에 다른 곳에서 새롭게 공부한 내용을 붙여가는 리펙터링은 생각보다 개념 이해에 도움이 많이 된다는 것을 알게 되었고, 얼른 리액트 애플리케이션과의 테스트를 해보면서 로그인 기능 구현까지 마무리하고 싶다.

728x90
Comments