ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringSecurity 06
    Spring&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가 가로채어 처리

     

     

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

    1. 스프링은 초기화 시 생성되는 전체 빈을 검사하면서 빈이 가진 메서드 중에서 보안이 설정된 메서드가 있는지 탐색한다
    2. 보안이 설정된 메소드가 있다면 스프링은 그 빈의 프록시 객체를 자동으로 생성한다 (기본적으로 Cglib 방식으로 생성한다)
    3. 보안이 설정된 메서드에는 인가처리 기능을 하는 Advice(MethodInterceptor)를 등록한다
    4. 스프링은 빈참조시 실제 빈이 아닌 프록시 빈 객체를 참조하도록 처리한다
    5. 초기화 과정이 종료된다
    6. 사용자는 프록시 객체를 통해 메서드를 호출하게되고 프록시 객체는 Advice가 등록된 메서드가 있다면 호출하여 작동 시킨다
    7. 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
Designed by Tistory.