본문 바로가기
관리자

Programming-[Backend]/Spring Security

[스프링 시큐리티]20. 실시간 권한 업데이트, 허용필터

728x90
반응형

1. 실시간 처리

 

실시간 처리는 관리자가 DB에 자원/권한 정보를 업데이트 할 때, 실시간으로 앞서 작성한 ResourceMap에 업데이트를 해서 사용자들이 웹 사이트를 이용하는데 문제가 없도록 하는 작업이다.

 

reload 메서드 추가

처리를 위해서 UrlFilterInvocationMetadataSource에 reload 메서드를 추가한다. iterator를 활용하는 문법에 유의하자.

 

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());
    }
}

 

 

ResourceController 처리

그리고 ResourceController에서 자원에 대한 정보 변경이 일어나므로 여기에서 reload 처리를 한다. UrlFilterInvocationMetadataSource를 불러오고, 자원 생성 시 reload() 메서드가 불려오도록 한다. 삭제 때도 reload 메서드만 추가해주면 된다.

 

@Controller
@RequiredArgsConstructor
public class ResourcesController {

    private final ResourcesService resourcesService;
    private final RoleService roleService;
    private final RoleResourceService roleResourceService;
    private final UrlFilterInvocationMetadataSource urlFilterInvocationMetadataSource;

    ...중략 ...

    @PostMapping(value="/admin/resource/register")
    public String createResource(RoleResourcesPo roleResourcesPo) throws Exception {

        ModelMapper modelMapper = new ModelMapper();
        RoleResourcesDto roleResourcesDto = modelMapper.map(roleResourcesPo, RoleResourcesDto.class);

        resourcesService.createRoleAndResources(roleResourcesDto);
        urlFilterInvocationMetadataSource.reload(); //자원 생성 시 reload

        return "redirect:/admin/resources";
    }

 

 

리소스 관리화면에서 권한 추가/삭제 시 권한이 바로 업데이트 되어 접근성이 실시간으로 달라지는 것을 확인할 수 있다. 관리자로 일반 브라우저에서 로그인하여 리소스를 관리하고, 크롬 시크릿 모드로 manager로 접속한 다음 /mypage에 있는 채로 대기한다. 관리자로 /mypage에 대한 권한을 삭제하면, manager가 다시 /mypage에 접근 시 아래와 같이 접근이 거부되었다는 메시지가 출력된다.

 

 


 

2. 허용 필터

 

앞서 살펴봤던 인증처리 방식은 FilterInvocationMetadataSource에서 요청 url에 따라 requestMap에 담긴 자원-권한 정보를 모두 살펴보는 방식으로 진행됐다. 만약 requestMap에 해당 자원에 대한 정보가 없다면 null을 리턴하여 모두 허용하는 방식을 적용했었다. 그러나, 만약 인가 처리를 하지 않아도 되는 자원에 대해서 이런 과정을 무조건 진행하게 한다면 리소스 낭비가 된다. 

 

기존 인증 및 권한 심사 과정은 대략 아래와 같았다.

 

FilterSecurityInterceptor -> AbstractSecurityInterceptor -> List<ConfigAttribute> (null이 아니라면) -> AccessDecisionManager

 

따라서 List<ConfigAttribute>를 검사하는 과정을 거치지 않도록 FilterSecurityInterceptor에서 미리 허용되는 자원들을 설정해주면 된다.

 

PermitAllFilter : FilterSecurityInterceptor 추가

permitAllFilter를 추가하고, FilterSecurityInterceptor를 상속받는다. FilterSecurityInterceptor의 코드를 복사해오고, 주석문 부분은 삭제하도록 한다.

 

*그리고 invoke 메서드에서도 super.beforeInvocation 부분을 부모의 메서드를 불러오는 것이 아니라 해당 클래스의 beforeInvocation 메서드를 불러오도록 해준다.

public class PermitAllFilter extends FilterSecurityInterceptor {
    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
//    private FilterInvocationSecurityMetadataSource securityMetadataSource;
    private boolean observeOncePerRequest = true;

    private List<RequestMatcher> permitAllRequestMatchers = new ArrayList<>();

    public PermitAllFilter(String...permitAllResources) {

        for (String permitAllResource : permitAllResources) {
            permitAllRequestMatchers.add(new AntPathRequestMatcher(permitAllResource));
        }
    }

    @Override
    protected InterceptorStatusToken beforeInvocation(Object object) {

        boolean permitAll = false;
        HttpServletRequest request = ((FilterInvocation) object).getRequest();
        for (RequestMatcher requestMatcher : permitAllRequestMatchers) {
            if (requestMatcher.matches(request)) {
                permitAll = true;
                break;
            }
        }

        if (permitAll) {
            return null;
        }

        //위 과정들을 모두 통과한다면 부모 클래스의 metadataSource 탐색 과정을 거치도록 한다.
        return super.beforeInvocation(object);
    }

//    public FilterSecurityInterceptor() {
//    }

//    public void init(FilterConfig arg0) {
//    }
//
//    public void destroy() {
//    }
//
//    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//        FilterInvocation fi = new FilterInvocation(request, response, chain);
//        this.invoke(fi);
//    }

//    public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
//        return this.securityMetadataSource;
//    }
//
//    public SecurityMetadataSource obtainSecurityMetadataSource() {
//        return this.securityMetadataSource;
//    }
//
//    public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
//        this.securityMetadataSource = newSource;
//    }
//
//    public Class<?> getSecureObjectClass() {
//        return FilterInvocation.class;
//    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            if (fi.getRequest() != null && this.observeOncePerRequest) {
                fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }

            //InterceptorStatusToken token = super.beforeInvocation(fi);
            InterceptorStatusToken token = beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, (Object)null);
        }

    }

//    public boolean isObserveOncePerRequest() {
//        return this.observeOncePerRequest;
//    }

//    public void setObserveOncePerRequest(boolean observeOncePerRequest) {
//        this.observeOncePerRequest = observeOncePerRequest;
//    }
}

 

생성자를 통해서 특별한 권한 없이도 접근이 가능한 url을 설정할 수 있도록 permitAllResources를 정의하고 permitAllRequestMatcher에 넣어주도록 하였다. 

 

그리고 beforeInvocation 메서드를 Override 해주었다. 여기서는 요청의 url이 인자로 받아오는 permitAllResources와 일치하는지 검사하고, 일치한다면 바로 null을 리턴하도록해서 부모의 beforeInvocation이 진행되지 않도록 해주었다. 이렇게 함으로써 request의 url에 대해 모든 resource를 검사하는 것이 아니라 인자로 받아오는 일부 permitAllResources와만 비교하게 된다.

 

 

SecurityConfig 설정

 

기존에 customFilterSecurityInterceptor에 적용했던 필터를 위에서 작성한 permitAllFilter로 변경해준다.

 

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;
    private final FormAuthenticationDetailsSource authenticationDetailsSource;
    private final FormAuthenticationSuccessHandler formAuthenticationSuccessHandler;
    private final FormAuthenticationFailureHandler formAuthenticationFailureHandler;
    private final SecurityResourceService securityResourceService;

    //permitAll 필터 사용을 위한 자원 설정
    private String[] permitAllResources = {"/", "/login", "/user/login/**"};

    //중략...

    @Bean
    public FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {

        //permitAllFilter 적용
        PermitAllFilter permitAllFilter = new PermitAllFilter(permitAllResources);
        permitAllFilter.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
        permitAllFilter.setAccessDecisionManager(affirmativeBased());
        permitAllFilter.setAuthenticationManager(authenticationManagerBean());

        return permitAllFilter;
    }

 

beforeInvocation 메서드에 break point를 걸고 "/" url로 접근해보자. 아래 콘솔에서 permitAllRequestMatchers에 SecurityConfig에서 설정해주었던 

 


 

 

참조

 

1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의

https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%E

 

 

728x90
반응형