ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringSecurity 05
    Spring&SpringBoot/SpringSecurity 2025. 9. 24. 13:03

    인가 프로세스 

    요청 기반 권한 부여 - HttpSecurity.authorizeHttpRequests()

    • Spring Security는 요청 기반 권한 부여와 메서드 기반 권한 부여를 통해 자원에 대한 심층적 방어 제공
    • 요청 기반 권한 부여는 클라이언트의 요청(HttpServletRequest)에 대한 권한 부여를 모델링하는 것이며 HttpSecurity 인스턴스를 사용하여 권한 규칙을 선언

     

     

    authorizeHttpRequests() API

    • requestMatchers()
      • Http 요청의 URL 패턴, HTTP 메서드, 요청 파라미터 등을 기반으로 어떤 요청에 대해서는 특정 보안 설정을 적용하고 다른 요청에 대해서는 적용하지 않도록 제어
      • 예) 특정 API 경로에만 CSRF 보호 적용하거나 특정 경로에 대해 이증 요구하지 않도록 설정
      • requestMatchers(String ... urlPatterns) : 자원 경로 한 개 이상 정의
      • requestMatchers(RequestMatcher ... requestMatchers) : AntPathRequstMatcher, MvcRequestMatcher 등 구현체 사용해서 자원 경로 한 개 이상 정의
      • requestMatchers(HttpMethod method, String ... urlPatterns) : Http Method와 자원 경로 한 개 이상 정의
    • 엔드포인트 & 권한부여
      • requestMatchers("/admin").hasRole("/ADMIN")

     

     

    @EnableWebSecurity
    @Configuration
    public class SecurityConfigV1 {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/user/{name}")
                    .access(new WebExpressionAuthorizationManager("#name == authentication.name"))
    
                    .requestMatchers("/admin/db")
                    .access(new WebExpressionAuthorizationManager("hasAythority('ROLE_DB') or hasAuthority('ROLE_ADMIN')"))
    
                    .anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults());
            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);
        }
    }

     

    @RestController
    public class IndexController {
        @GetMapping("/")
        public String index() {
            return "index";
        }
    
        @GetMapping("/user")
        public String user() {
            return "user";
        }
    
        @GetMapping("/user/{name")
        public String userName(@PathVariable(value = "name") String name) {
            return "name";
        }
    
        @GetMapping("/admin/db")
        public String admin() {
            return "admin";
        }
    }
    • 클라이언트의 요청에 대하여 위에서부터 아래로 나열된 순서대로 처리하며 요청에 대하여 첫 번째 일치만 적용되고 다음 순서로 넘어가지 않는다

     

     

    표현식 및 커스텀 권한 구현

    • 표현식을 사용해서 권한 규칙을 설정하도록 WebExpressionAuthorizationManager 제공
    • 표현식은 Security가 제공하는 권한 규칙을 사용하거나 사용자가 표현식을 커스텀하게 구현해서 설정 가능
    • requestMatchers().access(new WebExpressionAuthorizationManager("expression"))
    @Component("customWebSecurity")
    public class CustomWebSecurity {
        public boolean check(Authentication authentication, HttpServletRequest request) {
            return authentication.isAuthenticated;
        }
    }
    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http, ApplicationContext context) throws Exception {
            DefaultHttpSecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler();
            expressionHandler.setApplicationContext(context);
    
            WebExpressionAuthorizationManager authorizationManager = new WebExpressionAuthorizationManager("@customWebSecurity.check(authentication, request");
            authorizationManager.setExpressionHandler(expressionHandler);
    
            http.authorizeHttpRequests(authorize -> authorize
                            .requestMatchers("/custom/**").access(authorizationManager)
                    .anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults());
            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);
        }
    }

     

     

    커스텀 RequestMatcher 구현

    • RequestMacher의 matcher 및 matches 메서드를 사용해 클라이언트의 요청 객체로부터 값을 검증하도록 커스텀한  RequestMatcher를 구현하고 requestMatchers() 메서드에 설정
    public class CustomRequestMatcher implements RequestMatcher {
        private final String urlPattern;
    
        public CustomRequestMatcher(String urlPattern) {
            this.urlPattern = urlPattern;
        }
    
        @Override
        public boolean matches(HttpServletRequest request) {
            String requestURI = request.getRequestURI();
            return requestURI.startsWith(urlPattern);
        }
    }
    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http, ApplicationContext context) throws Exception {
            http.authorizeHttpRequests(authorize -> authorize
                            .requestMatchers(new CustomRequestMatcher("/admin")).hasAuthority("ROLE_ADMIN")
                            .anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults());
            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);
        }
    }

     

     

    요청 기반 권한 부여 - HttpSecurity.securityMatcher()

    securityMatcher()

    • securityMatcher(String .. urlPatterns)
    • securityMatcher(RequestMatcher .. requestMatchers)
    • http.sercurityMatcher("/api/**").authorizeHttpRequest(auth -> auth.requestMatchers(...))
      • Spring MVC가 클래스 경로에 있으면 MvcRequestMatcher가 사용되고 그렇지 않으면 AntPathRequestMatcher가 사용된다 

     

     

    securityMatchers(Customizer<RequestMatcherConfigurer>)

    • 특정 패턴에 해당하는 요청을 단일이 아닌 다중 설정으로 구성해서 보안 규칙을 적용할 수 있으며 현재의 규칙은 이전의 규칙을 대체하지 않는다
    • 패턴1. http.securityMatchers((matchers) -> matchers.requestMatchers("/api/**", "/oauth/**"));
    • 패턴2. http.securityMatchers((matchers) -> matchers.requestMatchers("/api/**"). requestMatchers( "/oauth/**"));
    • 패턴3. http.securityMatchers((matchers) -> matchers.requestMatchers("/api/**")).securityMatchers((matchers) -> matchers.requestMatchers(" /oauth/**" ));

     

     

    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth
                            .anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults())
            ;
            return http.build();
        }
    
        @Bean
        @Order(1)
        public SecurityFilterChain securityFilterChainV2(HttpSecurity http) throws Exception {
            http.securityMatchers(matchers -> matchers.requestMatchers("/api/**", "/oauth/**"))
                    .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll());
            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);
        }
    }
    @RestController
    @RequiredArgsConstructor
    public class IndexController {
    
        @GetMapping("/")
        public String index() {
            return "index";
        }
    
        @GetMapping("/custom")
        public String custom() {
            return "custom";
        }
    
        @GetMapping("/user/{name}")
        public String userName(@PathVariable(value="name") String name) {
            return name;
        }
    
        @GetMapping("/api/photos")
        public String photos() {
            return "photos";
        }
    
        @GetMapping("/oauth/login")
        public String oauth() {
            return "oauthLogin";
        }
    }

     

     

    메서드 기반 권한 부여 - @PreAuthorize, @PostAuthorize

    • @EnableMethodSecurity 어노테이션 추가
    • SpEL(Spring Expression Language) 표현식 사용

     

     

    @PreAuthorize

    • 메서드가 실행되기 전 특정한 보안 조건이 충족되는지 확인하는 데 사용
    • 보통 서비스 또는 컨트롤러 레이어의 메서드에 적용되어 해당 메서드가 호출되기 전 사용자의 인증 정보와 권한 검사
    @EnableWebSecurity
    @EnableMethodSecurity
    @Configuration
    public class SecurityConfigV1 {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth
                            .anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults())
            ;
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                    .password("{noop}1111")
                    .roles("USER").build();
    
            UserDetails db = User.withUsername("db")
                    .password("{noop}1111")
                    .roles("DB").build();
    
            UserDetails admin = User.withUsername("admin")
                    .password("{noop}1111")
                    .roles("ADMIN", "SECURE").build();
            return new InMemoryUserDetailsManager(user);
        }
    }
    @RestController
    public class MethodController {
        @GetMapping("/admin")
        @PreAuthorize("hasAuthority('ROLE_ADMIN')")
        public String admin() {
            return "admin";
        }
    
        @GetMapping("/user")
        @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')")
        public String user() {
            return "user";
        }
    
        @GetMapping("/isAuthenticated")
        @PreAuthorize("isAuthenticated")
        public String isAuthenticated() {
            return "isAuthenticated";
        }
    
        @GetMapping("/user/{id}")
        @PreAuthorize("#id == authentication.name")
        public String authentication(@PathVariable(name="id") String id) {
            return id;
        }
    }
    • 메서드 기반 보안 부여는 보통 service 쪽에서 많이 사용됨

     

     

    @PostAuthorize

    • 메서드가 실행된 후에 보안 검사 수행
    @Getter
    @AllArgsConstructor
    public class Account {
        private String owner;
        private boolean isSecure;
    }
    @RestController
    public class MethodController {
        ...
        
        @GetMapping("/owner")
        @PostAuthorize("returnObject.owner == authentication.name")
        public Account owner(String name) {
            return new Account(name, false);
        }
        
        @GetMapping("/isSecure")
        @PostAuthorize("hasAuthority('ROLE_ADMIN') and returnObject.isSecure")
        public Account isSecure(String name, String secure) {
            return new Account(name, "Y".equals(secure));
        }
    }

     

     

    메서드 기반 권한 부여 - @PreFilter, @PostFilter

    - @PreFilter : 메서드가 실행되기 전에 메서드에 전달된 컬렉션 타입의 파라미터에 대한 필터링 수행

    - @PostFilter : 메서드가 반환하는 컬렉션 타입의 결과에 대해 필터링 수행

    @EnableWebSecurity
    @EnableMethodSecurity
    @Configuration
    public class SecurityConfigV1 {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth
                            .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 db = User.withUsername("db")
                    .password("{noop}1111")
                    .roles("DB").build();
    
            UserDetails admin = User.withUsername("admin")
                    .password("{noop}1111")
                    .roles("ADMIN", "SECURE").build();
            return new InMemoryUserDetailsManager(user);
        }
    }
    @RestController
    @RequiredArgsConstructor
    public class MethodControllerV1 {
        private final DataService dataService;
    
        @PostMapping("/writeList")
        public List<Account> writeList(@RequestBody List<Account> data) {
            return dataService.writeList(data);
        }
    
        @PostMapping("/writeMap")
        public Map<String, Account> writeMap(@RequestBody List<Account> data) {
            Map<String, Account> accountMap = data.stream().collect(Collectors.toMap(account -> account.getOwner(), account -> account));
            return dataService.writeMap(accountMap);
        }
        
        @GetMapping("/readList")
        public List<Account> readList() {
            return dataService.readList();
        }
    }
    @Service
    public class DataService {
        @PreFilter("filterObject.owner == authentication.name")
        public List<Account> writeList(List<Account> data) {
            return data;
        }
    
        @PreFilter("filterObject.value.owner == authentication.name")
        public Map<String, Account> writeMap(Map<String, Account> data) {
            return data;
        }
        
        @PostFilter("filterObject.value.owner == authentication.name")
        public List<Account> readList() {
            return new ArrayList<>(List.of(
                    new Account("user", false),
                    new Account("db", false),
                    new Account("admin", false)
            ));
        }
    }
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class Account {
        private String owner;
        private boolean isSecure;
    }

     

     

    메서드 기반 권한 부여 - @Secured, JSR-250 및 부가 기능

    - @Secured

    • 지정된 권한(역할)을 가진 사용자만 해당 메서드를 호출할 수 있으며 더 풍부한 형식을 지원하는 @PreAuthorize 사용 권장
    • Spring Security 설정에서 @EnableMethodSecurity(securedEnable=true) 설정 활성화 필요

    - JSR-250 

    • @RolesAllowed, @PermitAll 및 @DenyAll 어노테이션 보안 기능 활성화
    • Spring Security 설정에서 @EnableMethodSecurity(jsr250Enable = ture) 설정 활성화 필요
    @RestController
    @RequiredArgsConstructor
    public class MethodController {
        private final DataService dataService;
    
        @GetMapping("/user")
        @Secured("ROLE_USER")
        public String writeList() {
            return "user";
        }
    
        @GetMapping("/admin")
        @RolesAllowed("ADMIN")
        public String admin() {
            return "admin";
        }
    
        @GetMapping("/permitAll")
        @PermitAll
        public String permitAll() {
            return "permitAll";
        }
    
        @GetMapping("/denyAll")
        @DenyAll
        public String denyAll() {
            return "denyAll";
        }
    }
    @EnableWebSecurity
    @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth
                            .anyRequest().permitAll())
                    .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 db = User.withUsername("db")
                    .password("{noop}1111")
                    .roles("DB").build();
    
            UserDetails admin = User.withUsername("admin")
                    .password("{noop}1111")
                    .roles("ADMIN", "SECURE").build();
            return new InMemoryUserDetailsManager(user);
        }
    }

     

     

    - 메타정보 추가

    @RestController
    @RequiredArgsConstructor
    public class MethodController {
        ...
    
        @GetMapping("/isAdmin")
        @IsAdmin
        public String isAdmin() {
            return "isAdmin";
        }
    
        @GetMapping("/ownerShip")
        @OwnerShip
        public Account ownerShip(String name) {
            return new Account(name, false);
        }
    }
    @Documented
    @Retention(RUNTIME)
    @Target({TYPE, METHOD})
    @PreAuthorize("hasRole('ADMIN')")
    public @interface IsAdmin {
    }
    @Documented
    @Retention(RUNTIME)
    @Target({TYPE, METHOD})
    @PostAuthorize("returnObject.owner == authentication.name")
    public @interface OwnerShip {
    }

     

     

    - 커스텀 빈을 사용하여 표현식 구하기

    @RestController
    @RequiredArgsConstructor
    public class MethodController {
        ...
        
        @GetMapping("/delete")
        @PreAuthorize("@myAuthorizer.isUser(#root)")
        public String delete() {
            return "delete";
        }
    }
    @Component("myAuthorizer")
    public class MyAuthorizer {
        public boolean isUser(MethodSecurityExpressionOperations root) {
            return root.hasAnyAuthority("ROLE_USER");
        }
    }

     

     

    정적 자원 관리

    • RequestMatcher 인스턴스를 등록하여 무시해야 할 요청을 지정할 수 있다
    • 주로 정적 자원(이미지, CSS, JavaScript 파일 등)에 대한 요청이나 특정 엔드포인트가 보안 필터를 거치지 않도록 설정할 때 사용
    • Ignoring 보다 permitAll 권장 : 정적 자원에 대한 요청일지라도 안전한 헤더를 작성할 수 있어 더 안전
    @EnableWebSecurity
    @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
    @Configuration
    public class SecurityConfig {
        ...
    
        @Bean
        public WebSecurityCustomizer webSecurityCustomizer() {
            return new WebSecurityCustomizer() {
                @Override
                public void customize(WebSecurity web) {
                    web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
                }
            };
        }
    }

     

     

     

    계층적 권한 - RoleHirerachy

    • 기본적으로 Spring Security에서 권한과 역할은 계층적이거나 상하 관계로 구분하지 않는다
      • 인증 주체가 다양한 역할과 권한을 부여받아야 한다
    • RoleHirerachy는 역할 간의 계층 구조를 정의하고 관리하는 데 사용되며 보다 간편하게 역할 간의 계층 구조를 설정하고 이를 기반으로 사용자에 대한 액세스 규칙을 정의할 수 있다
    • setHirerachy
      • 역할 계층을 설정하고 각 역할에 대해 해당 역할의 하위 계층에 속하는 모든 역할 집합을 미리 정해 놓는다
    • getReachableGrantedAuthorities
      • 모든 도달 가능한 권한의 배열을 반환한다
      • 도달 가능한 권한은 직접 할당된 권한에 더해 역할 계층에서 이들로부터 도달 가능한 모든 권한을 의미
    @RestController
    @RequiredArgsConstructor
    public class IndexController {
        @GetMapping("/user")
        public String user() {
            return "user";
        }
    
        @GetMapping("/db")
        public String db() {
            return "db";
        }
    
        @GetMapping("/admin")
        public String admin() {
            return "admin";
        }
    }
    @EnableWebSecurity
    @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
    @Configuration
    public class SecurityConfig {
        @Bean
        public RoleHierarchy roleHierarchy() {
            RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
            hierarchy.setHierarchy("ROLE_ADMIN > ROLE_DB\n" + "ROLE_DB > ROLE_USER\n" + "ROLE_USER > ROLE_ANONYMOUS\n");
            return hierarchy;
        }
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(authorize -> authorize
                            .requestMatchers("user").hasRole("USER")
                            .requestMatchers("db").hasRole("DB")
                            .requestMatchers("admin").hasRole("ADMIN")
                            .anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults())
                    .csrf(AbstractHttpConfigurer::disable)
            ;
            return http.build();
        }
    }
    728x90

    'Spring&SpringBoot > SpringSecurity' 카테고리의 다른 글

    SpringSecurity 06  (0) 2025.09.25
    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.