[스프링 시큐리티6] DB 연동 URL 방식인가 프로세스 + 계층 권한 적용

olive3·2024년 2월 8일
2

구현하고자 하는 것

  • DB와 연동하여 자원 및 권한을 설정하고 제어
  • RoleHierarchy를 통해 계층 권한 적용

DB

RoleHierarchy를 통해 상위 계층 Role은 하위 계층 Role 자원에 접근이 가능하므로 Resources와 Role은 다대일 단방향으로 구현했다.

테이블 연관관계

데이터

구현

스프링 시큐리티6 부터는 FilterSecurityInterceptor이 deprecated 되었다. 따라서 AuthorizationManager를 상속받아 DB로부터 자원 및 권한을 가져와 인가처리하도록 UrlAuthorizationManager를 구현했다.

UrlAuthorizationManager

public class UrlAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private LinkedHashMap<RequestMatcher, String> requestMap = new LinkedHashMap<>();
    private RoleHierarchy roleHierarchy = new NullRoleHierarchy();

    public UrlAuthorizationManager(LinkedHashMap<RequestMatcher, String> requestMap) {
        this.requestMap = requestMap;
    }

    public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> supplier, RequestAuthorizationContext requestAuthorizationContext) {

        Authentication authentication = supplier.get();
        HttpServletRequest request = requestAuthorizationContext.getRequest();

        if (requestMap != null) {
            for (Map.Entry<RequestMatcher, String> entry : requestMap.entrySet()) {
                RequestMatcher requestMatcher = entry.getKey();
                if (requestMatcher.matches(request)) {
                    String authority = entry.getValue();
                    boolean isGranted = isGranted(authentication, authority);
                    return new AuthorityAuthorizationDecision(isGranted, AuthorityUtils.createAuthorityList(authority));
                }
            }
        }

        return new AuthorizationDecision(true);
    }

    private boolean isGranted(Authentication authentication, String authority) {
        return authentication != null && isAuthorized(authentication, authority);
    }

    private boolean isAuthorized(Authentication authentication, String authority) {

        Iterator iter = getGrantedAuthorities(authentication).iterator();

        GrantedAuthority grantedAuthority;
        do {
            if (!iter.hasNext()) {
                return false;
            }

            grantedAuthority = (GrantedAuthority)iter.next();
        } while(!authority.equals(grantedAuthority.getAuthority()));

        return true;
    }

    private Collection<? extends GrantedAuthority> getGrantedAuthorities(Authentication authentication) {
        return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
    }
}
  • requestMap에 DB로 부터 가져온 자원/권한 정보가 Map형식으로 담겨있다.
  • 먼저 entry.getKey() 를 통해 RequestMatcher를 꺼내 사용자 요청 URI와 일치하는지 판단한다.
  • 일치한다면 entry.getValue() 를 통해 해당 자원에 해당하는 권한 정보를 꺼낸다. 그리고 해당 자원에 필요한 권한을 사용자가 가졌는지 isAuthorized() 메서드를 통해 확인한다.
  • 사용자가 적절한 권한을 가졌다면 isGranted = true, 그렇지 않다면 isGranted = false
  • 사용자 요청이 requestMap에 match 되는게 없다면 권한이 필요한 URI가 아니므로 AuthroizationDecision(true)를 반환한다.
  • getGrantedAuthorities() 메서드를 통해 계층
    권한이 적용된다. 디버깅을 통해 SecurityConfig에 설정한 계층권한이 적용되고 있음을 확인할 수 있다.

SecurityResourceService

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class SecurityResourceService {

    private final ResourcesRepository resourcesRepository;

    public LinkedHashMap<RequestMatcher, String> getResourceList() {

        LinkedHashMap<RequestMatcher, String> result = new LinkedHashMap<>();
        List<Resources> resourcesList = resourcesRepository.findAll();

        resourcesList.forEach(re -> {
            result.put(
                    new AntPathRequestMatcher(re.getResourceName()),
                    re.getRole().getRoleName());
        });

        return result;
    }
}
  • DB로 부터 자원/권한 정보를 가져온다.

ResourcesRepository

public interface ResourcesRepository extends JpaRepository<Resources, Long> {
}

UrlResourcesMapFactoryBean

public class UrlResourcesMapFactoryBean implements FactoryBean<LinkedHashMap<RequestMatcher, String>> {

    private SecurityResourceService securityResourceService;
    private LinkedHashMap<RequestMatcher, String> resourceMap;

    public void setSecurityResourceService(SecurityResourceService securityResourceService) {
        this.securityResourceService = securityResourceService;
    }

    @Override
    public LinkedHashMap<RequestMatcher, String> getObject() {
        if(resourceMap == null) {
            init();
        }

        return resourceMap;
    }

    private void init() {
        resourceMap = securityResourceService.getResourceList();
    }

    @Override
    public Class<?> getObjectType() {
        return LinkedHashMap.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}
  • FactoryBean을 통해 resourceMap을 빈으로 생성

SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final SecurityResourceService securityResourceService;

    //..생략

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers(new AntPathRequestMatcher("/**")).access(urlAuthorizationManager())
                        .anyRequest().authenticated()
                );

        http
                .formLogin(form -> form
                        .loginPage("/login")
                        .defaultSuccessUrl("/home")
                        .usernameParameter("loginId")
                        .passwordParameter("password")
                        .loginProcessingUrl("/login")
                        .permitAll()
                );

        http
                .csrf(CsrfConfigurer::disable);

        return http.build();
    }

    @Bean
    public UrlAuthorizationManager urlAuthorizationManager() {
        UrlAuthorizationManager urlAuthorizationManager = new UrlAuthorizationManager(urlResourcesMapFactoryBean().getObject());
        urlAuthorizationManager.setRoleHierarchy(roleHierarchy());
        return urlAuthorizationManager;
    }

    private UrlResourcesMapFactoryBean urlResourcesMapFactoryBean() {
        UrlResourcesMapFactoryBean urlResourcesMapFactoryBean = new UrlResourcesMapFactoryBean();
        urlResourcesMapFactoryBean.setSecurityResourceService(securityResourceService);
        return urlResourcesMapFactoryBean;
    }

    @Bean
    static RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        return hierarchy;
    }
}
  • .requestMatchers(new AntPathRequestMatcher("/**")).access(urlAuthorizationManager()) 모든 url을 UrlAurthorizationManagaer를 통해 인가처리한다.
  • UrlAurthorizationManagaer 도 빈으로 등록하다.
  • hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER") 에 원하는 계층구조를 세팅하면된다. >는 포함한다는 의미로 ROLE_ADMIN이 ROLE_USER 권한도 가진다는 의미한다.

이제 DB를 통해 자원/권한을 설정하고 제어할 수 있게되었다. 추가하고 싶은 자원/권한은 DB에 추가하면 된다. 또한 계층 구조도 > 으로 추가하면된다. 계층 구조는 아래 링크를 참고
Role Hierarchy

참고로 권한을 계층적으로 구성하지 않는다면 ROLE, RESOURCES 가 다대다 관계가 되므로 ROLE_RESOURCES 연결테이블을 추가해서 일대다, 다대일 관계로 풀면된다. 그리고 LinkedHashMap<RequestMatcher, String>을LinkedHashMap<RequestMatcher, List<String>> 으로 바꿔 구현하면 된다.

0개의 댓글