모르지 않다는 것은 아는것과 다르다.

Spring Security

URL 시큐리티 프로세스 커스텀

채마스 2022. 2. 28. 20:55

개요

  • SecurityConfig 파일에서 하나하나 mvcMatchers("/admin").hasRole("ADMIN") 이렇게 처리해 주지 않고, 동적으로 처리해주고 싶을 때가 있을것이다.
  • Spring Security 를 통해서 동적으로 권한을 부여해 줄 수 있도록 커스텀할 수 있다.





인가 처리 방식

  • 아래 그림은 인가 처리 방식을 나타낸 그림이다.

  • FilterInvocation
    • 요청 정보를 담는 객체이다.
    • FilterInvocation 객체는 FilterSecurityInterceptor 의 doFilter 메소드 안에 있다.
  • List attributes
    • 권한 정보를 담는 객체이다.
    • this.obtainSecurityMetadataSource().getAttribute(object); 에서 권한 정보를 가져온다.
  • Authentication, FilterInvocation, attributes 정보를 AccessDecisionManager 에게 전달한다.





SecurityMetadataSource

  • SecurityMetadataSource 는 최상의 인터페이스 로써, getAttributes, getAllConfigAttributes, supports 메소드를 가지고있다. -> 이 메소드를 재정의 해줌으로써 커스텀할 수 있다.
  • FilterInvocationSecurityMetadataSource
    • Url 권한 정보를 추출하는 인터페이스이다.
  • MethodSecurityMetadataSource
    • Method 권한 정보를 추출하는 인터페이스이다.





FilterInvocationSecurityMetadataSource

  • Url 권한 정보를 추출하는 인터페이스이다.
  • FilterInvocationSecurityMetadataSource 인터페이스를 구현한 클래스를 정의함으로써 커스텀할 수 있다.
  • RequestMap 객체를 가지고 있다. 이는 url : 권한목록 의 정보들이 담겨있는 map 이다.
  • 사용자가 접근하고자 하는 Url 자원에 대한 권한 정보 추출한다.
  • AccessDecisionManager 에게 전달하여 인가처리 수행한다.
  • DB 로부터 자원 및 권한 정보를 매핑하여 맵으로 관리한다.
  • 사용자의 매 요청마다 요청정보에 매핑된 권한 정보 확인한다.
  • 사용자에게 요청이 들어오면, FilterInvocationSecurityMetadataSource 에서 RequestMap 에 저장된 권한 정보를 불러와서 AccessDecisionManager 에게 전달해준다.





FilterInvocationSecurityMetadataSource 구현 코드

  • UrlFilterInvocationSecurityMetadatsSource 클래스 구현
@Slf4j
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap ;

    private SecurityResourceService securityResourceService;

    public UrlFilterInvocationSecurityMetadataSource(LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourcesMap, SecurityResourceService securityResourceService) {
        this.requestMap = resourcesMap;
        this.securityResourceService = securityResourceService;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {

        HttpServletRequest request = ((FilterInvocation) object).getRequest();

        if(requestMap != null){
            for(Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()){
                RequestMatcher matcher = entry.getKey();
                if(matcher.matches(request)){
                    return entry.getValue();
                }
            }
        }

        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        Set<ConfigAttribute> allAttributes = new HashSet<>();

        for (Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap
                .entrySet()) {
            allAttributes.addAll(entry.getValue());
        }

        return allAttributes;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

    public void reload(){

        LinkedHashMap<RequestMatcher, List<ConfigAttribute>> reloadedMap = securityResourceService.getResourceList();
        Iterator<Map.Entry<RequestMatcher, List<ConfigAttribute>>> iterator = reloadedMap.entrySet().iterator();

        requestMap.clear();

        while(iterator.hasNext()){
            Map.Entry<RequestMatcher, List<ConfigAttribute>> entry = iterator.next();
            requestMap.put(entry.getKey(), entry.getValue());
        }
    }
}
  • FilterInvocationSecurityMetadataSource 인터페이스를 구현한 클래스 이다.
  • 실제로 구현할 메소드는 getAttributes 이기 때문에 getAllConfigAttributes, supports 는 DefaultFilterInvocationSecurityMetadataSource 클래스에서 가져왔다.
  • requestMap 에 권한 정보를 세팅해 준다. -> 그 아래에서 해당 url과 매치 되는 권한을 리턴해 준다.
  • reload 메소드를 통해서 실시간으로 변경된 권한정보를 갱신해준다. -> requestMap 비워주고, 최신 정보로 갱신한다.
  • SecurityConfig 설정
    @Bean
    public FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {

        FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
        filterSecurityInterceptor.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
        filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
        filterSecurityInterceptor.setAuthenticationManager(authenticationManagerBean());
        return filterSecurityInterceptor;
    }

    private AccessDecisionManager affirmativeBased() {
        AffirmativeBased affirmativeBased = new AffirmativeBased(getAccessDecistionVoters());
        return affirmativeBased;
    }

    private List<AccessDecisionVoter<?>> getAccessDecistionVoters() {
        return Arrays.asList(new RoleVoter());
    }

    @Bean
    public FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource() {
        return new UrlFilterInvocationSecurityMetadatsSource();
    }

    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
  • FilterInvocationSecurityMetadataSource, AccessDecisionManager, AuthenticationManager 를 설정해준다.
.and()
        .addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class)
  • FilterSecurityInterceptor 앞에 직접 커스텀한 customFilterSecurityInterceptor 를 위치시킨다.
  • 그렇다면 FilterSecurityInterceptor 가 두번 실행 되지 않을까? -> 그렇지 않다. -> 한번 인가 처리가 되었으면 다음번엔 통과한다.





UrlResourcesMapFactoryBean

  • DB 연동 Map 기반으로 권한/ 자원 정보로 만들때 사용된다.
  • UrlResourcesMapFactoryBean
    • DB로 부터 얻은 권한/자원 정보를 -> ResourceMap 으로 만들고, 빈으로 생성해서 UrlFilterInvocationSecurityMetadataSource 에 전달한다.
  • 코드 예시
public class UrlResourcesMapFactoryBean implements FactoryBean<LinkedHashMap<RequestMatcher, List<ConfigAttribute>>> {

    private SecurityResourceService securityResourceService;
    private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourceMap;

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

    @Override
    public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getObject() throws Exception {

        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;
    }
}
  • isSingeton 메소드의 반환값을 true 로 지정해서 싱글톤타입으로 구성할 수 있다.
  • resourceMap 이 null 인 경우 resourceMap 을 가져올 수 있는 securityResourceService 를 호출한다.
  • securityResourceService 의 getResourceList 메소드에서 resourceMap 정보를 가져오는 코드는 아래와 같다.
public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getResourceList(){

    LinkedHashMap<RequestMatcher, List<ConfigAttribute>> result = new LinkedHashMap<>();
    List<Resources> resourcesList = resourcesRepository.findAllResources();
    resourcesList.forEach(re ->{
        List<ConfigAttribute> configAttributeList =  new ArrayList<>();
        re.getRoleSet().forEach(role -> {
            configAttributeList.add(new SecurityConfig(role.getRoleName()));
        });
        result.put(new AntPathRequestMatcher(re.getResourceName()),configAttributeList);
    });
    return result;
}
  • resourcesRepository.findAllResources() 는 아래와 같이 구현되어 있다.
    @Query("select r from Resources r join fetch r.roleSet where r.resourceType = 'url' order by r.orderNum desc")
    List<Resources> findAllResources();






PermitAllFilter 구현

 

public class PermitAllFilter extends FilterSecurityInterceptor {
    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
    private List<RequestMatcher> permitAllRequestMatcher = new ArrayList<>();
    public PermitAllFilter(String... permitAllPattern) {
        createPermitAllPattern(permitAllPattern);
    }

    @Override
    protected InterceptorStatusToken beforeInvocation(Object object) {
        boolean permitAll = false;
        HttpServletRequest request = ((FilterInvocation) object).getRequest();
        for (RequestMatcher requestMatcher : permitAllRequestMatcher) {
            if (requestMatcher.matches(request)) {
                permitAll = true;
                break;
            }
        }
        if (permitAll) return null;
        return super.beforeInvocation(object);
    }

    @Override
    public void invoke(FilterInvocation fi) throws IOException, ServletException {
            InterceptorStatusToken token = beforeInvocation(fi);
    }
    private void createPermitAllPattern(String... permitAllPattern) {
        for (String pattern : permitAllPattern) {
            permitAllRequestMatcher.add(new AntPathRequestMatcher(pattern));
        }
    }
}
  • FilterSecurityInterceptor 를 상속받고 invoke 메소드를 재정의한다.
  • invoke() 에서 FilterSecurityInterceptor 클래스의 부모 클래스인 AbstractSecurityInterceptor 의 beforeInvocation 를 아래와 같이 재정의 한다.
  • 생성자로 permitAllRequestMatcher 에 요청 정보를 넣어준다.
  • permitAllRequestMatcher 와 요청 정보를 비교해서 permitAll 처리를 해줄 요청을 검사한다.




REFERENCES

  • 정수원님의 스프링 시큐리티

'Spring Security' 카테고리의 다른 글

ProxyFactory 를 이용한 동적 Method 인가 처리  (0) 2022.02.28
Method 시큐리티 프로세스 커스텀  (0) 2022.02.28
권한 계층 적용하기  (0) 2022.02.28
Voter 커스텀 하기  (0) 2022.02.28
@AuthenticationPrincipal  (0) 2022.02.28