-
SpringSecurity 06Spring&SpringBoot/SpringSecurity 2025. 9. 25. 13:28
인가 아키텍처
인가 - Authorization
- 권한 부여는 특정 자원에 접근할 수 있는 사람을 결정하는 것
- GrantedAuthority 클래스를 통해 권한 목록을 관리하고 있으며 Authentication 객체와 연결한다
GrantedAuthority
- Aythentication에 GrantedAuthority 권한 목록을 저장하며 이를 통해 인증 주체에게 부여된 권한을 사용
- GrantedAuthority는 AuthenticationManager에 의해 Authentication 객체에 삽입하며 인가 결정을 내릴 때 AuthenticationManager를 사용해 인증 주체로부터 GrantedAuthority 객체를 읽어 들여 처리
- 기본적으로 역할 기반의 인가 규칙은 역할 앞에 ROLE_를 접두사로 사용한다
- GrantedAuthorityDefaults 로 사용자 지정할 수 있으며 GrantedAuthorityDefaults 는 역할 기반 인가 규칙에 사용할 접두사를 사용자 정의하는 데 사용된다
@Bean public GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults("MYPREFIX_"); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withUsername("user") .password("{noop}1111") .authorities("MYPREFIX_USER").build(); }인가 부여 관리자 - AuthorizationManager
- AuthorizationManager는 인증된 사용자가 요청 자원에 접근할 수 있는지 여부 결정하는 인터페이스
- AuthorizationManager는 Spring Security의 요청 기반, 메서드 기반의 인가 구성 요소에 의해 호출되며 최종 엑세스 제어 결정
- AuthorizationManager는 Spring Security의 필수 구성요소로서 권한 부여 처리는 AuthorizationFilter를 통해 이루어진다
- check() : 권한 부여 결정을 내릴 때 필요한 모든 관련 정보(인증객체, 체크 대상(권한정보, 요청정보, 호출정보 등..)가 전달
- verify() : check 를 호출해서 반환된 값이 false 가진 AuthorizationDecision 인 경우 AccessDeniedException을 throw
AuthorizationManager 클래스 계층 구조
AuthorizationManager : 특정 권한을 가진 사용자에게만 접근 허용
│
├── RequestMatcherDelegatingAuthorizationManager ( 요청 기반 권한 부여 관리자 )
│ │
│ ├── AuthenticatedAuthorizationManager : 특정 권한을 가진 사용자에게 접근 허용
│ ├── AuthorityAuthorizationManager : 인증된 사용자에게 접근 허용
│ └── WebExpressionAuthorizationManager : 웹 보안 표현식을 사용하여 권한 관리
│
└── PreAuthorizeAuthorizationManager ( 메서드 기반 권한 부여 관리자 )
│
├── PreAuthorizeAuthorizationManager : 메서드 실행 전 권한 검사 @PreAuthorize
├── PostAuthorizeAuthorizationManager : 메서드 실행 후 권한 검사 @PostAuthorize
├── Jsr250AuthorizationManager : JSR-250 어노테이션(@DenyAll, @PermitAll 등)을 사용하여 권한 관리
└── SecuredAuthorizationManager : @Secured 어노테이션으로 메서드 수준의 보안 제공요청 기반 인가 관리자
- 요청 기반의 인증된 사용자 및 특정 권한을 가진 사용자의 자원접근 허용 여부를 결정하는 인가 관리자 클래스 제공
- AuthorityAuthorizationManager, AuthenticatedAuthorizationManager, RequestMatcherDelegatingAuthorizationManager


AuthenticatedAuthorizationManager 구조
- FullyAuthenticatedAuthorizationStrategy : 익명 인증 및 기억하기 인증이 아닌 검사
- .requestMatchers("/myPage").fullyAuthenticated()
- AuthenticatedAuthorizationStrategy : 인증된 사용자인지 검사
- .requestMatchers("/user").authenticated()
- RemeberMeAuthorizationStrategy : 기억하기 인증인지 검사
- .requestMatchers("/history").rememberMe());
- AnonymousAuthorizationStrategy : 익명 사용자인지 검사
- .requestMatchers("/guest").anonymous()
요청 기반 Custom_AuthorizationManager 구현
- 인가 설정 시 선언적 방식이 아닌 프로그래밍 방식으로 구현할 수 있으며 access(AuthorizationManager) API 사용
- access()에는 AuthorizationManager 타입의 객체를 전달할 수 있으며 사용자의 요청에 대한 권한 검사를 access()에 지정한 AuthorizationManager가 처리
- access()에 지정한 AuthorizationManager 객체는 RequestMatcherDelegatingAuthorizationManager의 매핑 속성에 저장
- 적용 : .requestMatchers("/api").access(new CustomAuthorizationManager()));
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> { private final String REQUIRED_ROLE = "ROLE_SECURE"; @Override public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) { Authentication auth = authentication.get(); if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) { return new AuthorizationDecision(false); } boolean hasRequiredRole = auth.getAuthorities().stream().anyMatch(grantedAuthority -> REQUIRED_ROLE.equals(grantedAuthority.getAuthority())); return new AuthorizationDecision(hasRequiredRole); } }@EnableWebSecurity @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .requestMatchers("/user").hasRole("USER") .requestMatchers("/db").access(new WebExpressionAuthorizationManager("hasRole('DB')")) .requestMatchers("/admin").hasAuthority("ROLE_ADMIN") .requestMatchers("/api").access(new CustomAuthorizationManager()) .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) ; return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withUsername("user") .password("{noop}1111") .roles("USER").build(); UserDetails manager = User.withUsername("manager") .password("{noop}1111") .roles("MANAGER").build(); UserDetails admin = User.withUsername("admin") .password("{noop}1111") .roles("ADMIN").build(); return new InMemoryUserDetailsManager(user); } }RequestMatcherDelegatingAuthorizationManager 인가 설정 응용
- RequestMatcherDelegatingAuthorizationManager의 mappings 속성에 직접 RequestMatcherEntry 객체를 생성하고 추가한다
- RequestMatcherEntry<T>
- getEntry() : 요청 패턴에 매핑된 AuthorizationManager 객체 반환
- getRequestMatcher() : 요청 패턴에 저장한 RequestMatcher 객체 반환

좋은 구조는 아니지만 원리 이해를 위함 @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .anyRequest().access(authorizationManager(null))) .formLogin(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) ; return http.build(); } @Bean public AuthorizationManager<RequestAuthorizationContext> authorizationManager(HandlerMappingIntrospector introspector) { List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings = new ArrayList<>(); RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> requestMatcherEntry1 = new RequestMatcherEntry<>(new MvcRequestMatcher(introspector, "/user"), AuthoritiesAuthorizationManager.hasAuthority("ROLE_USER")); RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> requestMatcherEntry2 = new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, new AuthenticatedAuthorizationManager<>()); mappings.add(requestMatcherEntry1); mappings.add(requestMatcherEntry2); return new CustomRequestMatcherDelegatingAuthorizationManager(mappings); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withUsername("user") .password("{noop}1111") .roles("USER").build(); UserDetails manager = User.withUsername("manager") .password("{noop}1111") .roles("MANAGER").build(); UserDetails admin = User.withUsername("admin") .password("{noop}1111") .roles("ADMIN").build(); return new InMemoryUserDetailsManager(user, manager, admin); } }public class CustomRequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> { RequestMatcherDelegatingAuthorizationManager manager; public CustomRequestMatcherDelegatingAuthorizationManager(List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings) { manager = RequestMatcherDelegatingAuthorizationManager.builder().mappings(maps -> maps.addAll(mappings)).build(); } @Override public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) { return manager.check(authentication, object.getRequest()); } @Override public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) { AuthorizationManager.super.verify(authentication, object); } }메서드 기반 인가 관리자 - RequestMatcherDelegatingAuthorizationManager
- 메서드 기반의 인증된 사용자 및 특정권한을 가진 사용자의 자원접근 허용여부를 결정하는 인가 관리자 클래스들 제공
- PreAuthorizeAuthorizationManager, PostAuthorizeAuthorizationManager, Jsr250AuthorizationManager, SecuredAuthorizationManager
- 메서드 기반 권한 부여는 내부적으로 AOP 방식에 의해 초기화 설정이 이루어지며 메서드의 호출을 MethodInterceptor가 가로채어 처리

메서드 권한 부여 초기화 과정

- 스프링은 초기화 시 생성되는 전체 빈을 검사하면서 빈이 가진 메서드 중에서 보안이 설정된 메서드가 있는지 탐색한다
- 보안이 설정된 메소드가 있다면 스프링은 그 빈의 프록시 객체를 자동으로 생성한다 (기본적으로 Cglib 방식으로 생성한다)
- 보안이 설정된 메서드에는 인가처리 기능을 하는 Advice(MethodInterceptor)를 등록한다
- 스프링은 빈참조시 실제 빈이 아닌 프록시 빈 객체를 참조하도록 처리한다
- 초기화 과정이 종료된다
- 사용자는 프록시 객체를 통해 메서드를 호출하게되고 프록시 객체는 Advice가 등록된 메서드가 있다면 호출하여 작동 시킨다
- Advice는 메소드 진입 전 인가 처리를 하게 되고 인가처리가 승인되면 실제 객체의 메소드를 호출하게 되고 인가처리가 거부되면 예외가 발생하고 메소드 진입이 실패한다
메서드 기반 Custom AuthorizationManager 구현
@Service public class DataService { @PreAuthorize("hasAuthority('ROLE_USER')") public String getUser() { return "user"; } @PostAuthorize("returnObject.owner == authentication.name") public Account getOwner(String username) { return new Account(username, false); } public String display() { return "display"; } }@Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Account { private String owner; private boolean isSecure; }@RestController @RequiredArgsConstructor public class IndexController { private final DataService dataService; @GetMapping("/") public String index() { return "index"; } @GetMapping("/user") public String user() { return dataService.getUser(); } @GetMapping("/owner") public Account owner(String name) { return dataService.getOwner(name); } @GetMapping("/display") public String display() { return dataService.display(); } }@EnableWebSecurity @Configuration @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) public class SecurityConfig { @Bean public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations()); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) ; return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withUsername("user") .password("{noop}1111") .roles("USER").build(); UserDetails manager = User.withUsername("manager") .password("{noop}1111") .roles("MANAGER").build(); UserDetails admin = User.withUsername("admin") .password("{noop}1111") .roles("ADMIN").build(); return new InMemoryUserDetailsManager(user, manager, admin); } }메서드 기반 Custom AuthorizationManager 구현
- 사용자 정의 AuthorizationManager를 생성함으로 메서드 보안을 구현할 수 있다
public class MyPreAuthorizationManager implements AuthorizationManager<MethodInvocation> { @Override public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation object) { Authentication auth = authentication.get(); if (auth instanceof AnonymousAuthenticationToken) { return new AuthorizationDecision(false); } return new AuthorizationDecision(auth.isAuthenticated()); } }public class MyPostAuthorizationManager implements AuthorizationManager<MethodInvocationResult> { @Override public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult object) { Authentication auth = authentication.get(); if (auth instanceof AnonymousAuthenticationToken) { return new AuthorizationDecision(false); } Account account = (Account) auth.getPrincipal(); boolean isGrandted = account.getOwner().equals(auth.getName()); return new AuthorizationDecision(isGrandted); } }포인트 컷 메서드 보안 구현하기 - AspectJExpressionPoincut / ComposablePointcut
- 자체 어드바이저(Advisor)를 발행하거나 포인트컷(PointCut)을 사용하여 AOP 표현식을 애플리케이션의 인가 규칙에 맞게 매칭할 수 있으며 이를 통해 어 노테이션을 사용하지 않고도 메서드 수준에서 보안 정책을 구현할 수 있다
# 의존성 주입
dependencies {
implementation "org.springframework.boot:spring-boot-starter-aop"
}@EnableMethodSecurity(prePostEnabled = false) @Configuration public class MethodSecurityConfig { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public Advisor pointCutAdvisor() { AspectJExpressionPointcut pattern = new AspectJExpressionPointcut(); pattern.setExpression("execution(* io.security.security_ex.v2.DataService.getUser(..))"); AuthorityAuthorizationManager<MethodInvocation> manager = AuthorityAuthorizationManager.hasRole("USER"); return new AuthorizationManagerBeforeMethodInterceptor(pattern, manager); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public Advisor pointCutAdvisor() { AspectJExpressionPointcut pattern = new AspectJExpressionPointcut(); pattern.setExpression("execution(* io.security.security_ex.v2.DataService.getUser(..))"); AspectJExpressionPointcut patternV2 = new AspectJExpressionPointcut(); patternV2.setExpression("execution(* io.security.security_ex.v2.DataService.getOwner(..))"); ComposablePointcut composablePointcut = new ComposablePointcut((Pointcut) pattern); composablePointcut.union((ClassFilter) patternV2); AuthorityAuthorizationManager<MethodInvocation> manager = AuthorityAuthorizationManager.hasRole("USER"); return new AuthorizationManagerBeforeMethodInterceptor(composablePointcut, manager); } }@Service public class DataService { // @PreAuthorize("hasAuthority('ROLE_USER')") public String getUser() { return "user"; } // @PostAuthorize("returnObject.owner == authentication.name") public Account getOwner(String username) { return new Account(username, false); } public String display() { return "display"; } }AOP 메서드 보안 구현 - MethodInterceptor, Pointcut, Advisor
- MethodInterceptor, Pointcut, Advisor, AuthorizationManager 등을 커스텀하게 생성하여 AOP 메서드 보안을 구현할 수 있다

- Advisor : AOP Advice와 Advice 적용 가능성을 결정하는 포인트컷을 가진 기본 인터페이스
- MethodInterceptor(Advice) : 대상 객체를 호출하기 전 후 추가 작업을 수행하기 위한 인터페이스로서 Joinpoint.proceed() 호출
- Pointcut : AOP에서 Advice가 적용될 메서드나 클래스를 정의하는 것으로 어드바이스가 실행되어야 하는 적용지점 혹은 조건
- ClassFilter와 MethodMatcher를 사용해서 어떤 클래스 및 어떤 메서드에 Advice를 적용할 것인지 결정
public class CustomMethodInterceptor implements MethodInterceptor { private final AuthorizationManager<MethodInvocation> authorizationManager; public CustomMethodInterceptor(AuthorizationManager<MethodInvocation> authorizationManager) { this.authorizationManager = authorizationManager; } @Override public Object invoke(MethodInvocation invocation) throws Throwable { Authentication authentication = SecurityContextHolder.getContextHolderStrategy().getContext().getAuthentication(); if (authorizationManager.check(() -> authentication, invocation).isGranted()) { return invocation.proceed(); } throw new AccessDeniedException("Access denied"); } }@EnableMethodSecurity(prePostEnabled = false) @Configuration public class MethodSecurityConfig { @Bean public MethodInterceptor methodInterceptor() { AuthorizationManager<MethodInvocation> authorizationManager = new AuthenticatedAuthorizationManager<>(); return new CustomMethodInterceptor(authorizationManager); } @Bean public Pointcut pointcut() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(* io.security.security_ex.v2.DataService.*(..))"); return pointcut; } @Bean public Advisor serviceAdvisor() { return new DefaultPointcutAdvisor(pointcut(), methodInterceptor()); } }728x90'Spring&SpringBoot > SpringSecurity' 카테고리의 다른 글
SpringSecurity 05 (0) 2025.09.24 SpringSecurity 04 (0) 2025.09.18 SpringSecurity 03 (0) 2025.09.17 SpringSecurity 02 (0) 2025.09.16 SpringSecurity 01 (0) 2025.09.15