이 시리즈에 나오는 모든 내용은 인프런 인터넷 강의 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 에서 기반된 것입니다. 그리고 여기서 인용되는 PPT 이미지 또한 모두 해당 강의에서 가져왔음을 알립니다.
인증을 받은 사용자가 어떤 자원에 접근 시, 접근자격이 있는지를 증명하는 것
큰 흐름으로 보면 위 그림과 같이 인가 처리를 하게 된다.
스프링 시큐리티는 총 3가지의 계층(web,service, domain)에 대한
인가 처리를 지원한다.
참고) FilterChainProxy.java 에서 디버그를 잘 잡으면 아래처럼 필터 리스트를
볼 수 있다. FilterSecurityInterceptor 가 가장 마지막에 있는 것을 확인할 수 있다.
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
메소드를 호출하게 된다.
해당 메소드의 내용을 계속 따라가면 아래와 같은 메소드들을 호출하게 된다.
Voter
라는 존재에 의해서 접근 찬/반 여부가 결정 (추후에 더 자세히)이 예외는 필터를 타고 올라가서 ExceptionTranslationFilter 의 try-catch
에
잡히게 되고, 아래와 같은 처리를 하게 된다.
현재는 비로그인 사용자이므로 sendStartAuthentication 메소드가 실행되게 된다.
참고로 로그인을 해도 똑같이 AccessDeniedException 이 터지고,
위 그림에서 맨 아래에 있는 this.accessDeniedHandler.handle 메소드가 수행된다.