상세 컨텐츠

본문 제목

AuthroizationFilter를 이용한 인가(AuthorizationManager, RequestMatcherDelegatingAuthorizationManager)

Spring/Spring Security

by Chan.94 2024. 12. 1. 18:11

본문

반응형

Intro

AuthorizationFilter가 도입되면서, 기존의 FilterSecurityInterceptor를 대체할 수 있는 옵션이 제공되었다.
이 변화는 Spring Security의 현대적이고 유연한 보안 구성 방식을 반영하며, 간결하고 효율적인 권한 관리를 가능하게 한다.

 

FilterSecurityInterceptor를 이용한 구현은 아래 포스팅을 참고하기 바란다.

[Spring/Spring Security] - Spring Security - FilterSecurityInterceptor 이해 및 Example

 

Spring Security - FilterSecurityInterceptor 이해 및 Example

IntroSpring Security의 인증과 인가에 대한 내용을 복기해 보자.Authentication(인증) : 해당 사용자가 본인이 맞는지 확인하는 절차. Authorization(인가) : 인증된 사용자가 요청된 자원에 접근가능한가를 결

fvor001.tistory.com

 

비교

  FilterSecurityInterceptor AuthorizationFilter
역할 URL 요청에 대한 권한 확인 및 접근 제어 URL 요청 권한 확인을 좀 더 간단하고 명시적으로 수행
구성 방식 기존의 필터 기반 구성 (SecurityMetadataSource 등) HttpSecurity.authorizeHttpRequests()를 사용한 선언적 구성
유연성 복잡한 커스터마이징 가능 (but, 설정이 복잡할 수 있음) 간결하고 선언적이며 최신 요구사항에 적합

 

AuthroizationFilter

public class AuthorizationFilter extends GenericFilterBean {

    private final AuthorizationManager<HttpServletRequest> authorizationManager;

    private AuthorizationEventPublisher eventPublisher = AuthorizationFilter::noPublish;
    
    /**
     * Creates an instance.
     * @param authorizationManager the {@link AuthorizationManager} to use
     */
    public AuthorizationFilter(AuthorizationManager<HttpServletRequest> authorizationManager) {
        Assert.notNull(authorizationManager, "authorizationManager cannot be null");
        this.authorizationManager = authorizationManager;
    }
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws ServletException, IOException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        ...
        
        try {
            AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
            this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
            if (decision != null && !decision.isGranted()) {
                throw new AccessDeniedException("Access Denied");
            }
            chain.doFilter(request, response);
        }
        finally {
            request.removeAttribute(alreadyFilteredAttributeName);
        }
    }
    
    ...
}

 

  • AuthorizationManager의 check() 메서드를 호출하여 적절한 권한 부여 여부를 체크
  • URI 기반으로 권한 부여 처리를 하는 AuthorizationFilter는 AuthorizationManager의 구현 클래스로 RequestMatcherDelegatingAuthorizationManager를 사용한다.

AuthorizationFilter 설정 Example

@Configuration
@EnableWebSecurity
public class SecurityConfig extends SecurityFilterChain {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated()
            )
            ...
        return http.build();
    }
}

 

 

AuthorizationManager

@FunctionalInterface
public interface AuthorizationManager<T> {

    default void verify(Supplier<Authentication> authentication, T object) {
        AuthorizationDecision decision = check(authentication, object);
        if (decision != null && !decision.isGranted()) {
            throw new AccessDeniedException("Access Denied");
        }
    }

    @Nullable
    AuthorizationDecision check(Supplier<Authentication> authentication, T object);

}

AuthorizationManager는 AuthorizationFilter에서 권한 확인을 처리한다.

AuthorizationManager를 구현하여 더 세부적인 권한 제어를 구현할 수 있다.

 

RequestMatcherDelegatingAuthorizationManager

public final class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<HttpServletRequest> {

    private final Log logger = LogFactory.getLog(getClass());

    private final List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings;

    private RequestMatcherDelegatingAuthorizationManager(
            List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings) {
        Assert.notEmpty(mappings, "mappings cannot be empty");
        this.mappings = mappings;
    }
    
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Authorizing %s", request));
        }
        for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {

            RequestMatcher matcher = mapping.getRequestMatcher();
            MatchResult matchResult = matcher.matcher(request);
            if (matchResult.isMatch()) {
                AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager));
                }
                return manager.check(authentication,
                        new RequestAuthorizationContext(request, matchResult.getVariables()));
            }
        }
        this.logger.trace("Abstaining since did not find matching RequestMatcher");
        return null;
    }
    
    ...
}
  • RequestMatcherDelegatingAuthorizationManager는 AuthorizationManager의 구현 클래스 중 하나
  • 직접 권한 부여 처리를 수행 하지 않고 RequestMatcher를 통해 매치되는 AuthorizationManager 구현 클래스에게 권한 부여 처리를 위임한다.
  • 여기서 RequestMatcher는 SecurityConfig에서 requestMatchers("/admin/**").hasRole("ADMIN")와 같은 메서드 체인 정보를 기반으로 생성된다.

SecurityConfig.java

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity//Spring Security 사용
public class SecurityConfig {

    @Value("${permit.url}")
    private final String[] PERMIT_ALL_RESOURCES;
    
    private final JwtProvider jwtProvider;
    
    private final RequestHeader requestHeader;
    
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    /**
     * SecurityFilterChain 정의
     */
    @Bean
    protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            .httpBasic(httpBasic -> httpBasic.disable())   
            .formLogin(formLogin -> formLogin.disable())    
            .csrf(csrf -> csrf.disable())       
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

            /*
             * 인가(Authorization)
             */
            .authorizeHttpRequests(authorize -> authorize
                    .antMatchers(PERMIT_ALL_RESOURCES).permitAll()
                    .requestMatchers(new AntPathRequestMatcher("/**")).access(new UrlAuthorizationManager())
                    .anyRequest().authenticated())
            /**
             * exception handling
             * 인증, 인가 과정에서 발생할 수 있는 예외를 처리하는 방법을 설정
             */
            .exceptionHandling(exception -> exception
                    .accessDeniedHandler(new JwtAccessDeniedHandler())              //인가(Authorization) 실패시
                    .authenticationEntryPoint(new JwtAuthenticationEntryPoint()))   //인증(Authentication) 실패시
            // JWT 인증 필터
            .addFilterBefore(new JwtAuthenticationFilter(jwtProvider, requestHeader, PERMIT_ALL_RESOURCES), UsernamePasswordAuthenticationFilter.class)
            ;
        
        return httpSecurity.build();
    }

}

 

  • 인가 체크를 제외할 URL(PERMIT_ALL_RESORCES)에 대해서는 허용
  •  나머지 URL에서는 AuthorizationManager를 구현하여 진행한다. 

UrlAuthorizationManager.java (AuthorizationManager구현체)

@Slf4j
public class UrlAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    // URL 패턴과 해당 권한을 매핑할 Map
    private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();

    public UrlAuthorizationManager() {
        // URL 패턴과 권한 매핑 (예시)
        requestMap.put(new AntPathRequestMatcher("/admin/**"), Collections.singletonList(new SecurityConfig("ADMIN")));
        requestMap.put(new AntPathRequestMatcher("/api/v1/**"), Arrays.asList(new SecurityConfig("ADMIN"), new SecurityConfig("USER")));
    }
    
    @Override
    public AuthorizationDecision check(Supplier<Authentication> supplier, RequestAuthorizationContext requestAuthorizationContext) {
        Authentication authentication = supplier.get();
        HttpServletRequest httpServletRequest = requestAuthorizationContext.getRequest();

        if(authentication.getAuthorities().isEmpty()) {
            return new AuthorizationDecision(false);
        }
        Collection<ConfigAttribute> attributes = this.getRequestAttribute(httpServletRequest);

        boolean containAuthority = false;
        SecurityConfig securityConfig = null;
        
        for (final ConfigAttribute configAttribute : attributes) {
            if (configAttribute instanceof SecurityConfig) {
                securityConfig = (SecurityConfig) configAttribute;
                for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                    containAuthority = securityConfig.getAttribute().equals(grantedAuthority.getAuthority());
                    if (containAuthority) {
                        break;
                    }
                }
                if (containAuthority) {
                    break;
                }
            }
        }
        
        return new AuthorizationDecision(containAuthority);
        
    }

    private Collection<ConfigAttribute> getRequestAttribute(HttpServletRequest request) {
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
            if (entry.getKey().matches(request)) {
                return entry.getValue();
            }
        }
        return null;
    }
}

 

  • 임의로 생성자에 두개의 URL에 대해 권한을 부여한 후 테스트를 진행하였다.
반응형

관련글 더보기

댓글 영역

>