[Spring Security] 아키텍처 이해 - Authorization, FilterSecurityInterceptor

식빵·2022년 9월 8일
0

이 시리즈에 나오는 모든 내용은 인프런 인터넷 강의 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 에서 기반된 것입니다. 그리고 여기서 인용되는 PPT 이미지 또한 모두 해당 강의에서 가져왔음을 알립니다.



1. Authorization(인가)?

인증을 받은 사용자가 어떤 자원에 접근 시, 접근자격이 있는지를 증명하는 것

큰 흐름으로 보면 위 그림과 같이 인가 처리를 하게 된다.

  • 먼저 인증을 받았는지 체크하고,
  • 그 사용자의 권한을 보고 자원에 대한 접근이 가능한지를 판단한다.



2. 권한 체킹과 계층

스프링 시큐리티는 총 3가지의 계층(web,service, domain)에 대한
인가 처리를 지원한다.

  • 웹 계층: URL 요청에 따른 인가 처리
  • 서비스 계층 : 메소드 단위의 인가처리
  • 도메인 계층: 객체 단위의 인가처리



3. FilterSecurityInterceptor

  • 인가처리를 담당하는 필터이다.
  • 시큐리티 필터들 중 마지막에 위치하며 인증된 사용자의 특정 요청 승인/거부 최종 결정
  • 인증객체 없이 보호자원에 접근 시도하면 AuthenticationException
  • 인증 후 자원 접근 권한이 없을 경우 AccessDeniedException
  • 권한 제어 방식 중 HTTP 자원의 보안을 처리
  • 권한 처리를 AccessDecisionManager 에게 위임


참고) FilterChainProxy.java 에서 디버그를 잘 잡으면 아래처럼 필터 리스트를
볼 수 있다. FilterSecurityInterceptor 가 가장 마지막에 있는 것을 확인할 수 있다.




4. 간단 테스트


4-1. application.properties

spring.security.user.name=user
spring.security.user.password=1111
# spring.security.user.roles=USER

4-2. SpringConfig

package me.dailycode.securitymemo;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/favicon.ico");
        web.ignoring().mvcMatchers("/error");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/user").hasRole("USER")
            .anyRequest().permitAll();
        http.formLogin();
    }
}



4-3. 샘플 Controller

@RestController
public class SecurityController {
    @GetMapping("/")
    public String index() {
        return "home";
    }
    
    @GetMapping("/user")
    public String user() {
        return "user";
    }
}



localhost:8080/user 로 접근하면...


4-4 디버깅 및 코드흐름 추적

먼저 FilterSecurityInterceptor 가 부모 클래스에게 뭔가를 요청하는 걸 확인

참고) 부모클래스 AbstractSecurityInterceptor는 현재 보고 있는 FilterSecurityIntercpetor 와같이 Filter 형식의 자식 클래스뿐만 아니라
aop 를 지원하는 MethodSecurityInterceptor 자식 클래스도 존재한다.


그리고 부모 클래스의 beforeInvocation 메소드 내부에서 일어나는 일들은 아래와 같다.


Collection<ConfigAttribute> attributes 
	= this.obtainSecurityMetadataSource().getAttributes(object);
    
if (CollectionUtils.isEmpty(attributes)) {
    Assert.isTrue(!this.rejectPublicInvocations,
                  () -> "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));
    }
    publishEvent(new PublicInvocationEvent(object));
    return null; // no further work post-invocation
    
    // .... 중간 생략 ....
    
    attemptAuthorization(object, attributes, authenticated);
}

먼저 자원에 접근하기 위해서 필요한 권한 정보가 무엇인지를 알아낸다. (obtainSecurityMetadataSource)

그리고나서 만약에 아무런 권한이 필요없는 자원이라면 return null을 하고 끝낸다.

하지만 권한이 필요한 자원이라면 추후에 attemptAuthorization 메소드를 호출하게 된다.
해당 메소드의 내용을 계속 따라가면 아래와 같은 메소드들을 호출하게 된다.

  • accessDecisionManager 에게 인가처리를 위임하는 것을 확인
  • 인가처리에 대해서 Voter 라는 존재에 의해서 접근 찬/반 여부가 결정 (추후에 더 자세히)
    • 지금의 경우에는 로그인 없이 접근해서 AccessDeniedException 이 터진다.

이 예외는 필터를 타고 올라가서 ExceptionTranslationFilter 의 try-catch
잡히게 되고, 아래와 같은 처리를 하게 된다.


현재는 비로그인 사용자이므로 sendStartAuthentication 메소드가 실행되게 된다.
참고로 로그인을 해도 똑같이 AccessDeniedException 이 터지고,
위 그림에서 맨 아래에 있는 this.accessDeniedHandler.handle 메소드가 수행된다.

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글