Untitled1.class

Authorize HttpServletRequests 본문

공부/Spring Security

Authorize HttpServletRequests

kitti-zini 2025. 5. 13. 15:47

Spring Security를 사용하면 Request 수준에서 권한 부여를 모델링할 수 있다. 예를 들어, Spring Security를 사용하면 /admin 아래의 모든 page에는 하나의 권한이 필요하고, 다른 모든 page에는 인증만 필요하다고 정의할 수 있다.

기본적으로 Spring Security는 모든 요청에 대한 인증을 요구한다. 하지만 HttpSecurity instance를 사용할 때는 항상 권한 부여 규칙을 선언해야 한다.

HttpSecurity instance가 있는 경우, 최소한 다음 작업을 수행해야 한다:

http
	.authorizeHttpRequests((authorize) -> authorize
		.anyRequest().authenticated()
	)

이는 Spring Security에 Application의 모든 endpoint가 최소한 보안 컨텍스트 인증을 받아야 허용된다는 것을 알려준다.

Understanding How Request Authorization Components Work

  1. 먼저 AuthorizationFilter는 SecurityContextHolder에서 Authentication을 검색하는 Supplier를 구성한다.
  2. Supplier<Authentication>과 HttpServletRequest를 AuthorizationManager에 전달한다. AuthorizationManager는 Request를 authorizeHttpRequests 패턴과 일치시키고 해당 규칙을 실행한다.
    1. 권한 부여가 거부되면 AuthorizationDeniedEvent가 발행되고 AccessDeniedException이 발생한다. 이 경우 ExceptionTranslationFilter가 AccessDeniedException을 처리한다.
    2. 접근 권한이 부여되면 AuthorizationGrantedEvent가 발행되고 AuthorizationFilter는 FilterChain을 계속 실행하여 Application이 정상적으로 처리될 수 있도록 한다.

AuthorizationFilter Is Last By Default

AuthorizationFilter는 기본적으로 Spring Security Filter Chain의 마지막에 위치한다. 즉, Spring Security의 인증 Filter, exploit 보호 및 기타 Filter 통합은 권한 부여를 필요로 하지 않는다.

AuthorizationFilter 앞에 Filter를 추가하면 해당 Filter 역시 권한 부여를 필요로 하지 않는다. 그렇지 않으면 권한이 부여된다.

All Dispatches Are Authorized

AuthorizationFilter는 모든 Request 뿐만 아니라 모든 Dispatch에서도 실행된다. 즉, REQUEST Dispatch는 권한 부여 뿐만 아니라 FORWARD, ERROR, INCLUDE도 필요하다.

예를 들어, Spring MVC는 다음과 같이 Thymeleaf 템플릿을 렌더링하는 View Resolver로 Request를 FORWARD할 수 있다:

@Controller
public class MyController {
    @GetMapping("/endpoint")
    public String endpoint() {
        return "endpoint";
    }
}

이 경우 권한 부여는 두 번 발생한다. 한 번은 /endpoint 권한을 부여하기 위해, 다른 한 번은 “endpoint” 템플릿을 렌더링하기 위해 Thymeleaf로 전달하기 위해 발생한다.

따라서 모든 FORWARD Dispatch를 허용할 수 있다.

이 원칙의 또 다른 예는 Spring Boot가 오류를 처리하는 방식이다. Container에서 예외가 발생하면 다음과 같이 처리한다:

@Controller
public class MyController {
    @GetMapping("/endpoint")
    public String endpoint() {
        throw new UnsupportedOperationException("unsupported");
    }
}

그러면 Boot가 해당 ERROR Dispatch를 처리한다.

이 경우 권한 부여도 두 번 발생한다. 한 번은 /endpoint 권한을 부여하기 위해, 다른 한 번은 오류를 처리하기 위해 발생한다.

따라서 모든 ERROR Dispatch를 허용하는 것이 좋다.

Authentication Loocup is Deferred

AuthorizationManager API는 Supplier<Authentication>을 사용한다는 점을 기억하라.

authorizeHttpRequests에서 Request가 항상 허용되거나 거부되는 경우 이 점이 중요하다. 이 경우 Authentication은 쿼리되지 않으므로 Request 속도가 빨라진다.

Authorizing an Endpoint

Spring Security에서 우선순위에 따라 규칙을 추가하여 다양한 규칙을 적용하도록 설정할 수 있다.

USER 권한을 가진 최종 사용자만 /endpoint에 접근할 수 있도록 하려면 다음과 같이 한다:

@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
	    .requestMatchers("/endpoint").hasAuthority("USER")
            .anyRequest().authenticated()
        )
        // ...

    return http.build();
}

선언은 패턴/규칙 쌍으로 나눌 수 있다.

AuthorizationFilter는 나열된 순서대로 이러한 쌍을 처리하여 첫 번째로 일치하는 항목만 Request에 적용한다. 즉, /**가 /endpoint와도 일치하더라도 위 규칙은 문제가 되지 않는다. 위 규칙을 읽는 방법은 “Request가 /endpoint이면 USER 권한이 필요하고, 그렇지 않으면 인증만 필요하다”이다.

Spring Security는 여러 패턴과 규칙을 지원하며, 각각을 프로그래밍 방식으로 직접 만들 수도 있다.

권한이 부여되면 다음과 같이 Security의 Test 지원을 사용하여 Test할 수 있다:

@WithMockUser(authorities="USER")
@Test
void endpointWhenUserAuthorityThenAuthorized() {
    this.mvc.perform(get("/endpoint"))
        .andExpect(status().isOk());
}

@WithMockUser
@Test
void endpointWhenNotUserAuthorityThenForbidden() {
    this.mvc.perform(get("/endpoint"))
        .andExpect(status().isForbidden());
}

@Test
void anyWhenUnauthenticatedThenUnauthorized() {
    this.mvc.perform(get("/any"))
        .andExpect(status().isUnauthorized());
}

Authorizing Requests

Request가 매칭되면 permitAll, denyAll, hasAuthority 등 이미 확인된 여러 가지 방법으로 권한을 부여할 수 있다.

간단히 요약하자면, DSL에 내장된 권한 부여 규칙은 다음과 같다:

  • permitAll: Request에 권한이 필요하지 않으며 공개 endpoint이다. 이 경우 Session에서 인증 정보를 가져오지 않는다.
  • denyAll: Request는 어떠한 상황에서도 허용되지 않는다. 이 경우 Session에서 인증 정보를 가져오지 않는다.
  • hasAuthority: Request는 인증 정보에 지정된 값과 일치하는 GrantedAuthority를 요구한다.
  • hasRole: hasAuthority의 바로가기이며, ROLE_ 또는 기본 접두사로 구성된 값을 접두사로 붙인다.
  • hasAnyAuthority: Request는 인증 정보에 지정된 값 중 하나와 일치하는 GrantedAuthority를 요구한다.
  • hasAnyRole: hasAnyAuthority의 바로가기이며, ROLE_ 또는 기본 접두사로 구성된 값을 접두사로 붙인다.
  • access: Request는 이 사용자 지정 AuthorizationManager를 사용하여 액세스를 결정한다.
import static jakarta.servlet.DispatcherType.*;

import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
	http
		// ...
		.authorizeHttpRequests(authorize -> authorize                                  (1)
            .dispatcherTypeMatchers(FORWARD, ERROR).permitAll() (2)
			.requestMatchers("/static/**", "/signup", "/about").permitAll()         (3)
			.requestMatchers("/admin/**").hasRole("ADMIN")                             (4)
			.requestMatchers("/db/**").access(allOf(hasAuthority("db"), hasRole("ADMIN")))   (5)
			.anyRequest().denyAll()                                                (6)
		);

	return http.build();
}
  1. 여러 개의 권한 부여 규칙이 지정되어 있다. 각 규칙은 선언된 순서대로 고려된다.
  2. FORWARD와 ERROR Dispatch는 Spring MVC가 View를 렌더링하고 Spring Boot가 오류를 렌더링할 수 있도록 허용된다.
  3. 모든 사용자가 액세스할 수 있는 여러 URL 패턴을 지정했다. 특히 URL이 “/static/”으로 시작하거나, “/signup”과 같거나, “/about”과 같으면 모든 사용자가 요청에 액세스할 수 있다.
  4. “/admin/”으로 시작하는 모든 URL은 “ROLE_ADMIN”역할을 가진 사용자로 제한된다. hasRole method를 호출하므로 “ROLE_”접두사를 지정할 필요가 없다.
  5. “/db/”로 시작하는 모든 URL은 사용자에게 “db” 권한과 “ROLE_ADMIN” 권한이 모두 부여되어야 한다. hasRole 표현식을 사용하므로 “ROLE_” 접두사를 지정할 필요가 없다.
  6. 아직 매칭되지 않은 URL은 액세스가 거부된다. 실수로 권한 규칙을 업데이트하는 것을 잊어버리는 일이 발생하지 않도록 하려면 이 방법이 좋다.

참조

https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html#request-authorization-architecture