HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🤩
개발
/
🔐
Spring Security
/
📑
FilterSecurityInterceptor(인가 처리)
📑

FilterSecurityInterceptor(인가 처리)

FilterSecurityInterceptorAccessDecisionManager 인터페이스ConfigAttribute 인터페이스(SpEL로 정해준 정보가 담기는 곳)AccessDecisionVoter 인터페이스SpEL 표현식 소개커스텀 SpEL 생성 & 적용
인가(Authorization) — 어플리케이션 보안을 이해하는데 두 번째로 중요한 핵신 개념으로(다른 하나는 인증) 권한이 부여된 사용자들만 특정 기능 또는 데이터에 접근을 허용하는 기능이다. 이를 위해 인가 처리는 두 개의 작업으로 구분된다.
  • 인증된 사용자와 권한을 매핑해야 함 — Spring Security에서는 보통 역할이라고 함 (예: ROLE_USER, ROLE_ADMIN, ROLE_ANONYMOUS)
  • 보호되는 리소스에 대한 권한 확인 — 관리자 권한을 가진 사용자만 관리자 페이지에 접근 가능
Spring Security 3.0 — Dmitry Noskov
Spring Security 3.0 — Dmitry Noskov

FilterSecurityInterceptor

FilterSecurityInterceptor
  • 필터 체인 상에서 가장 마지막에 위치하며, 사용자가 갖고 있는 권한과 리소스에서 요구하는 권한을 취합하여 접근을 허용할지 결정함
    • 실질적으로 접근 허용 여부 판단은 AccessDecisionManager 인터페이스 구현체에서 이루어짐
  • 작동방식
    • FilterSecurityInterceptor의 doFilter()에서 invoke() 호출
    • invoke() 에서 AbstractSecurityInterceptor의 beforeInvocation() 호출
    • attemptAuthorization() 에서 AccessDecisionManager의 decide() 호출
    • 해당 메서드에서 이 요청이 통과해도 되는 요청인지 투표함(AccessDecisionVoter)
// FilterSecurityInterceptor public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException { if (isApplied(filterInvocation) && this.observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()); return; } // first time this request being called, so perform security checking if (filterInvocation.getRequest() != null && this.observeOncePerRequest) { filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(filterInvocation); try { filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } //beforeInvocation => AbstractSecurityInterceptor protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass()); } else { Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); if (CollectionUtils.isEmpty(attributes)) { Assert.isTrue(!this.rejectPublicInvocations, () -> { return "Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. This indicates a configuration error because the rejectPublicInvocations property is set to 'true'"; }); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Authorized public object %s", object)); } this.publishEvent(new PublicInvocationEvent(object)); return null; } else { if (SecurityContextHolder.getContext().getAuthentication() == null) { this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } Authentication authenticated = this.authenticateIfRequired(); if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes)); } this.attemptAuthorization(object, attributes, authenticated); /* ... */ } private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, Authentication authenticated) { try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException var5) { if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object, attributes, this.accessDecisionManager)); } else if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes)); } this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var5)); throw var5; } }
FilterSecurityInterceptor.invoke( ) → AbstractSecurityInterceptor.beforeInvocation( ) → AbstractSecurityInterceptor.attemptAuthorization( )
  • 해당 필터가 호출되는 시점에서 사용자는 이미 인증이 완료되고, Authentication 인터페이스의 getAuthorities() 메소드를 통해 인증된 사용자의 권한 목록을 가져올수 있음
    • 익명 사용자도 인증이 완료된 것으로 간주하며, ROLE_ANONYMOUS 권한을 갖음
  • 보호되는 리소스에서 요구하는 권한 정보는 SecurityMetadataSource 인터페이스를 통해 ConfigAttribute 타입으로 가져옴
    • Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
[Introduction to Spring Method Security]
  • FilterSecurityInterceptor : antMatcher로 정의되는 url에 대한 permission을 보고 처리함
  • MethodSecurityInterceptor : @PreAuthorize 혹은 @PostAuthorize 와 같은 어노테이션이 붙어있는 메서드에 대해 처리를 함
  • SecurityMetadataSource : 권한 판정을 하기 위해서는 Config attribute가 필요한데, 그것을 모아놓은 map임
    • FilterSecurityInterceptor와 MethodSecurityInterceptor가 각각 다른 SecurityMetadataSource를 가지게 됨
  • MethodSecurityInterceptor를 가능하게 해주는 어노테이션@EnableGlobalMethodSecurity(prePostEnabled = true)
    • 해당 어노테이션은 @Configuration 클래스에다가 붙여서 사용해야 함
  • 권한 체크는 FilterSecurityInterceptor에서 한번, 그리고 @PreAuthorize 어노테이션이 붙어있는 곳에서 반복적으로 다 체크를 하게 됨
notion image
Filter 권한 위원회 = FilterSecurityInterceptor. Global Method 권한 위원회 = MethodSecurityInterceptor
Filter 권한 위원회 = FilterSecurityInterceptor. Global Method 권한 위원회 = MethodSecurityInterceptor

AccessDecisionManager 인터페이스

AccessDecisonManager
notion image
  • 사용자가 갖고 있는 권한과 리소스에서 요구하는 권한을 확인하고, 사용자가 적절한 권한을 갖고 있지 않다면 접근 거부 처리함
  • AccessDecisionVoter 목록을 갖고 있음
  • AccessDecisionVoter들의 투표(vote)결과를 취합하고, 접근 승인 여부를 결정하는 3가지 구현체를 제공함
    • notion image
    • AffirmativeBased — AccessDecisionVoter가 승인하면 이전에 거부된 내용과 관계없이 접근이 승인됨 (기본값)
    • ConsensusBased — 다수의 AccessDecisionVoter가 승인하면 접근이 승인됨
    • UnanimousBased — 모든 AccessDecisionVoter가 만장일치로 승인해야 접근이 승인됨
    • @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) throws AccessDeniedException { int grant = 0; List<ConfigAttribute> singleAttributeList = new ArrayList<>(1); singleAttributeList.add(null); for (ConfigAttribute attribute : attributes) { singleAttributeList.set(0, attribute); for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, singleAttributeList); /* ... */ }
CustomAccessDecisionManager & Custom Voter
  • CustomAccessDecisionManager 등록 방법 [Custom AccessDecisionVoter in Spring Security-Baeldung]
    • http.authorizeRequests() .antMatchers("/me").hasAnyRole("USER", "ADMIN") .antMatchers("/admin").access("hasRole('ADMIN') and isFullyAuthenticated()") .anyRequest().permitAll() .accessDecisionManager(new UnanimousBased(List.of( new WebExpressionVoter(), new OddAdminVoter()))) .and()
  • Customer Voter 코드
    • package com.prgms.devcourse.springsecuritymasterclass.config; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.util.matcher.RequestMatcher; import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.regex.Matcher; import java.util.regex.Pattern; public class OddAdminVoter implements AccessDecisionVoter<FilterInvocation> { private static final Pattern PATTERN = Pattern.compile("[0-9]$"); private final RequestMatcher requestMatcher; public OddAdminVoter(RequestMatcher requestMatcher) { this.requestMatcher = requestMatcher; } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } @Override public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) { if(!requiresAuthorization(object.getRequest())) return ACCESS_GRANTED; User user = (User)authentication.getPrincipal(); String username = user.getUsername(); Matcher matcher = PATTERN.matcher(username); if(matcher.find()){ int number = Integer.parseInt(matcher.group()); if(number % 2 != 0){ return ACCESS_GRANTED; } return ACCESS_DENIED; } return ACCESS_ABSTAIN; } private boolean requiresAuthorization(HttpServletRequest request){ return requestMatcher.matches(request); } }

ConfigAttribute 인터페이스(SpEL로 정해준 정보가 담기는 곳)

ConfigAttribute 인터페이스
notion image
  • WebExpressionConfigAttribute가 ConfigAttribute인터페이스의 구현체 중 하나임
    • http.authorizeRequests() .antMatchers("/me").hasAnyRole("USER", "ADMIN") .antMatchers("/admin").access("hasRole('ADMIN') and isFullyAuthenticated()") .anyRequest().permitAll()
    • 위의 hasAnyRole 부분과 access 부분의 정보가 WebExpressionConfigAttribute구현체로 생성되어 AccessDeicisionVoter에게 넘어감

AccessDecisionVoter 인터페이스

AccessDecisionVoter 인터페이스
  • 각각의 AccessDecisionVoter는 접근을 승인할지 거절할지 혹은 보류할지 판단함 (vote 메소드)
    • int ACCESS_GRANTED = 1; int ACCESS_ABSTAIN = 0; int ACCESS_DENIED = -1; int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
      AccessDecisionVoter 구현 일부 발췌
    • ACCESS_GRANTED — 접근 승인
    • ACCESS_DENIED — 접근 거부
    • ACCESS_ABSTAIN — 판단 보류 (판단을 위한 정보 부족 등)

SpEL 표현식 소개

SpEL 표현식 소개
  • WebExpressionVoter 구현체
    • [Expression-Based Access Control 스프링 문서]
    • SpEL 표현식을 사용해 접근 승인 여부에 대한 규칙을 지정할 수 있음
    • SpEL 표현식 처리를 위해 DefaultWebSecurityExpressionHandler 그리고 WebSecurityExpressionRoot 구현에 의존함
      • DefaultWebSecurityExpressionHandler.createSecurityExpressionRoot() 메소드에서 WebSecurityExpressionRoot 객체를 생성함
    • WebSecurityExpressionRoot 클래스는 SpEL 표현식에서 사용할수 있는 다양한 메소드를 제공

    커스텀 SpEL 생성 & 적용

    SpEL 표현식 커스텀 핸들러 구현
    WebSecurityExpressionRoot를 상속하는 클래스 생성
    • Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 요청을 승인하는 SpEL 표현식을 구현
      • admin01 — 접근 허용
      • admin02 — 접근 거부
    • WebSecurityExpressionRoot를 상속하고, Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 요청을 승인하는 isOddAdmin() 메소드를 추가함
    public class CustomWebSecurityExpressionRoot extends WebSecurityExpressionRoot { static final Pattern PATTERN = Pattern.compile("[0-9]+$"); public CustomWebSecurityExpressionRoot(Authentication a, FilterInvocation fi) { super(a, fi); } public boolean isOddAdmin() { User user = (User) getAuthentication().getPrincipal(); String name = user.getUsername(); Matcher matcher = PATTERN.matcher(name); if (matcher.find()) { int number = toInt(matcher.group(), 0); return number % 2 == 1; } return false; } }
    AbstractSecurityExpressionHandler를 상속하는 클래스 생성. CustomWebSecurityExpressionRoot 객체를 생성하는 CustomWebSecurityExpressionHandler구현체 (DefaultWebSecurityExpressionHandler의 구현 형태를 보고 참고해서 만들면 됨)
    public class CustomWebSecurityExpressionHandler extends AbstractSecurityExpressionHandler<FilterInvocation> { private final AuthenticationTrustResolver trustResolver; private final String defaultRolePrefix; public CustomWebSecurityExpressionHandler(AuthenticationTrustResolver trustResolver, String defaultRolePrefix) { this.trustResolver = trustResolver; this.defaultRolePrefix = defaultRolePrefix; } @Override protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) { CustomWebSecurityExpressionRoot root = new CustomWebSecurityExpressionRoot(authentication, fi); root.setPermissionEvaluator(getPermissionEvaluator()); root.setTrustResolver(this.trustResolver); root.setRoleHierarchy(getRoleHierarchy()); root.setDefaultRolePrefix(this.defaultRolePrefix); return root; } }
    커스텀 SpEL 표현식 (oddAdmin)을 추가하고, CustomWebSecurityExpressionHandler 를 설정(expresionHandler( ))
    • "isFullyAuthenticated() and hasRole('ADMIN') and oddAdmin" — 명시적 로그인을 수행한 Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 허용
    • SpEL에서 isOddAdmin( ), oddAdmin 이 두 방식으로 호출이 가능함
    http // ... 생략 ... .authorizeRequests() .antMatchers("/me").hasAnyRole("USER", "ADMIN") .antMatchers("/admin").access("isFullyAuthenticated() and hasRole('ADMIN') and oddAdmin") .anyRequest().permitAll() .expressionHandler(new CustomWebSecurityExpressionHandler(new AuthenticationTrustResolverImpl(), "ROLE_")) .and() // ... 생략 ...
    • expressionHandler를 제대로 설정하지 않으면 ExpressionUtils.evaluateAsBoolean() 메소드에서 예외가 발생함
      • DEBUG 로그 레벨로는 확인할 수 있으며, ExpressionUtils 클래스에 브레이크 포인트를 걸어두고 확인할 수 있음
    ⚠️
    org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'oddAdmin' cannot be found on object of type 'org.springframework.security.web.access.expression.WebSecurityExpressionRoot' - maybe not public or not valid?