본문 바로가기

Spring Boot/Security

인가 아키텍처 : 인가 프로세스

 - Spring Security의 인가 처리 흐름

 - URL 기반 인가 설정 방법

 - 메서드 보안을 통해 세밀한 인가 제어 방식 이해

 - 인가 이벤트와 예외 처리 전략

 - 실무 환경에서 발생할 수 있는 인가 실패 상황과 해결법


개요

Spring Security Filter Chain에 도달한 사용자의 인증 요청을 처리하기 위한 작업이 수행된 후, 인증된 사용자임을 확인했다고 했을때, 이 인증된 사용자는 이제 애플리케이션에서 제공하는 리소스를 마음대로 이용할 수 있는가?

 

단순히 인증에만 성공했다고 해서 모든 리소스에 접근할 순 없다. 권한 부여(인가, Authorization)라는 중요한 보안 요소가 있음


Spring Security의 컴포넌트로 보는 권한 부여(Authorization) 처리 흐름

인가 처리 흐름

 

  • Spring Security Filter Chain에서 URL을 통해 사용자의 액세스를 제한하는 권한부여 Filter가 AuthorizationFilter

  • AuthorizationFilter는 먼저 SecurityContextHolder로부터 Authentication을 획득한다 (1)

  • SecurityContextHolder로부터 획득한 Authentication과 HttpServletRequest를 AuthorizationManager에게 전달함 (2)
  • AuthorizationManager는 권한 부여 처리를 총괄하는 매니저 역할을 하는 인터페이스이고 RequestMatcherDelegatingAuthorizationManagerAuthorizationManager를 구현하는 구현체중 하나이다

  • RequestMatcherDelegatingAuthorizationManager는 RequestMatcher 평가식을 기반으로 해당 평가식에 매치되는 AuthorizationManager에게 권한 부여 처리를 위임하는 역할을 한다
    • RequestMatcherDelegatingAuthorizationManager직접 권한 부여 처리를 하는게 아니라,
      RequestMatcher를 통해 매치되는 AuthorizationManager 구현 클래스에게 위임만 한다는 사실을 꼭 기억해야함
  • RequestMatcherDelegatingAuthorizationManager 내부에서 매치되는 AuthorizationManager 구현 클래스가 있다면
    해당 AuthorizationManager 구현 클래스가 사용자의 권한을 체크한다 (3)

  • 적절한 권한이라면, 다음 요청 프로세스를 계속 이어간다 (4)

  • 만약 적절한 권한이 아니라면 AccessDeniedException이 throw되고 ExceptionTranslationFilter가 AccessDeniedException을 처리하게 된다 (5)

인가 처리 흐름에서 사용되는 컴포넌트의 세부 흐름

1. FilterSecurityInterceptor와 AuthorizationFilter

SpringSecurity의 인가 과정은 보통 필터체인에서 수행된다

 

주요 필터

  • FilterSecurityInterceptor : 가장 전통적인 인가 처리 필터로, URL 요청에 대한 접근 권한을 검사한다
  • AuthorizationFilter : Spring Security 5.5+ 에서 추가된 새로운 필터로, AuthorizationManager와 함께 동작한다

AuthorizationFilter

 

FilterSecurityInterceptor와 AuthorizationFilter 모두 요청이 들어올 때 인증 정보를 기반으로 권한 검사를 수행한다

 

 

2. 인가 결정 위임 메커니즘

인가는 보통 AuthorizationManager 혹은 AccessDecisionManager에게 위임된다

필터는 단순히 "인가 검사 필요" 신호를 보내고, 실제 권한 판단은 매니저가 수행한다고 함

 

 

3. 인가 성공 / 실패시 처리방식

인가 성공시 요청은 다음 필터나 컨트롤러로 정상 전달된다.

인가가 실패하면 AccessDeniedException이 발생하며, AccessDeniedHandler가 이를 처리한다고 함

 

 

4. 팁

기존 프로젝트에서는 FilterSecurityInterceptor 기반 코드가 많다

신규 프로젝트에서는 AuthorizationFilter + AuthorizationManager 조합을 권장한다고 함

 

컴포넌트 설명
FilterSecurityInterceptor 전통적 인가 필터
AuthorizationFilter 최신 인가 필터(AuthorizationManager 기반)
AccessDecisionManager / AuthorizationManager 실제 인가 판단 수행

 


URL 기반 인가 설정

1. requestMatchers().access() 메서드 활용하기

URL 요청 경로별로 권한을 제어할 수 있다

 

Spring Security 6(Spring Boot 3.xx) 에서는 antMatchers, mvcMatchers, regexMatchers 가 제거되고
requestMatchers() 한가지로 통일되었다

내부적으로 어떤 Matchers를 사용하느냐에 따라 Ant / MVC / Regex 전략이 적용된다고 한다

.authorizeHttpRequests(auth -> auth
    .requestMatchers(PathRequest.toH2Console()).permitAll()
    .requestMatchers("/admin/**").access(AuthorityAuthorizationManager.hasRole("ADMIN"))
    .requestMatchers("/user/**").access(AuthorityAuthorizationManager.hasRole("USER"))
    .anyRequest().authenticated())

 

 

동일한 목적을 더욱 축약해서 빌더 메서드로도 표현할 수 있다.

.authorizeHttpRequests(auth -> auth
    .requestMatchers(PathRequest.toH2Console()).permitAll()
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .requestMatchers("/user/**").hasRole("USER")
    .anyRequest().authenticated())

 

표현 장점 단점 추천 상황
.access(AuthorizationManager) 복잡한 조건(AND / OR, 커스텀 로직) 조합이 쉽다 코드가 길어질 수 있음 데이터 기반 조건
커스텀 매니저가 필요할 때
.hasRole / .hasAuthority /
.authenticated()
간결하고 읽기 쉽다 복잡한 조건이 제한됨 일반적인 권한 맵핑

 

 

 

2. 경로 매칭 전략(Ant / MVC / Regex)

(1) Ant 패턴(가장 흔함)

http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/**").hasRole("USER")
    .requestMatchers("/admin/*").hasRole("ADMIN")
    .anyRequest().authenticated()
);

 

  • * : 경로 요소 1개
  • ** : 하위 모든 디렉토리를 포함함

REST API나 단순 경로 매칭에 적합하다. 무엇보다 편하고 단순함

 

 

(2) MVC 패턴(컨텍스트 경로 / 로케일 고려)

import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
    
     /**
     * 경로 매칭 전략, MVC 패턴
     * MvcRequestMatcher는 3.5.x 기준으로 더이상 사용되지 않는다
     */
    
    @Bean
    MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
        return new MvcRequestMatcher.Builder(introspector);
    }
    
    @Bean
    SecurityFilterChain springSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers(mvc().pattern("/articles/{id}")).hasRole("USER") // 경로 변수 인식
                .requestMatchers(mvc().pattern(HttpMethod.POST, "/articles")).hasRole("WRITER")
                .anyRequest().authenticated()
        );
        return http.build();
    }

 

Spring MVC 컨트롤러 맵핑 규칙을 그대로 반영할 수 있다. 따라서 유지보수성이 뛰어남

 

 

 

(3) Regex 패턴(고급 케이스)

.authorizeHttpRequests(auth -> auth
    .requestMatchers(new RegexRequestMatcher("^/file/[a-f0-9\\\\-]{36}$", null)).hasAuthority("FILE_READ")
)

 

유연하지만 한눈에 봐도 복잡하므로 필요한 경우에만 제한적으로 사용할 수 있다.

 

구분 특징
Ant 패턴 단순한 와일드카드 기반 /api/**
MVC 패턴 컨트롤러 맵핑 / 경로 변수 반영 /articles/{id}
Regex 패턴 정규표현식 기반 ^/file/[0-9]+$

 

 

 

 

3. 정적 리소스와 API 경로 보호

정적 리소스(css, js, images 등등)는 보통 permitAll()로 허용시켜준다

Spring Boot에서 제공하는 PathRequest, Actuator의 EndpointRequest를 활용하면 더욱 간편하게 설정할 수 있다

 

// 단순한 화이트리스트 방식
http.authorizeHttpRequest(auth -> auth
    .reqeustMatchers("/css/**", "/js/**", "/images/**").permitAll()
    .anyRequest().authenticated()
);


// PathRequest 활용하기(Spring Boot에서 제공해줌)
http.authorizeHttpRequests(auth -> auth
    .requstMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
);


// Actuator 예시
/**
* implementation 'org.springframework.boot:spring-boot-starter-actuator'
* import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
*/
http.authorizeHttpRequests(auth -> auth
    .requestMatchers(EndPointRequest.to("health","info")).permitAll()
    .requestMatchers(EndPointRequest.toAnyEndPoint()).hasRole("ADMIN")
    .anyRequest().authenticated()
);

 

이렇게 하면 정적 리소스와 공개용 API는 누구나 접근 가능하고, 그 외의 자원은 인증된 사용자만 접근할 수 있게 된다

 

 

 

4. 규칙 평가 순서와 우선 순위

Spring Security는 위에서 아래로 규칙을 평가하며, 먼저 매칭된 규칙이 최종적으로 적용된다

Spring Security 규칙 적용 순서

 

구체적인 경로를 먼저 선언하고, 일반적인 규칙은 아래에 배치하는 것이 안전하다.

 

 

 

5. permitAll vs anonymous vs authenticated

키워드 의미 전형적 사용
permitAll() 로그인 여부와 무관하게 모두 허용함 정적 리소스, 공개 API
anonymous() 로그인하지 않은 사용자만 허용함 로그인 / 회원가입 페이지
authenticated() 로그인 사용자만 허용함 일반 보호 API
denyAll() 누구도 접근 불가함 임시 차단, 테스트

 

http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/login","/signup").anonymous()
    .requestMatchers("/assets/**").permitAll()
    .anyRequest().authenticated()
);

 

 

 

6. 팁

  • API 경로와 정적 리소스를 반드시 구분하고, 보안 대상만 잠궈야한다
  • 규칙은 구체적인 경로부터 작성하고, 일반적인 규칙은 마지막에 배치해야함
  • permitAll, anonymous, authenticated 는 의미가 다르므로 상황에 맞게 선택해야한다

 

개념 설명
requestMatchers().access() 경로별 권한 설정하기
Ant / MVC / Regex 경로 매칭 전략 선택하기
정적 리소스 화이트리스트 permitAll, PathRequest, EndpointRequest 활용하기
규칙 평가 순서 위 - > 아래, 구체적 - > 일반적
permitAll / anonymous / authenticated 각각 의미가 달라서 적절히 사용해야함

메서드 보안

1. @EnableMethodSecurity 활성화

URL 기반 인가 외에도 메서드 단위 보안을 적용할 수 있다

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

}

 

위 설정을 통해서 @PreAuthorize, @PostAuthorize 애너테이션을 사용할 수 있게 된다

 

 

 

2. @PreAuthorize, @PostAuthorize 활용하기

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long id) {
        // ADMIN 권한을 가진 사용자만 실행 가능한 메서드
    }

    @PostAuthorize("returnObject.owner == authentication.name")
    public User getUser(Long id) {
        // 반환된 User 객체의 소유자가 현재 사용자일때만 접근 허용하기
    }

 

* @PreAuthorize : 메서드 실행 전 인가 검사하기

* @PostAuthorize : 메서드 실행 후 반환값 기반 인가 검사하기

 

 

 

3. 메서드 파라미터 기반 동적 인가

  • Spring Security의 메서드 보안은 SpEL(Spring Expression Language)를 사용하여 메서드 파라미터를 조건으로 활용할 수 있다
  • 컨트롤러나 서비스 메서드에 전달된 파라미터 이름은 SpEL에서 그대로 참조할 수 있다
  • 현재 인증된 사용자의 정보는 authenticaion 또는 principal 키워드를 통해 접근할 수 있다
@PreAuthorize("#id == authentication.principal.id") // 여기
public void updateUser(Long id) {
	// 현재 로그인한 사용자와 id가 같을 때만 실행됨
}

 

 

(1) 동작 원리

  1. 스프링 AOP가 메서드의 호출을 가로챔
  2. 메서드 실행 전, @PreAuthorize 안의 SpEL 표현식을 평가한다
  3. SpEL 컨텍스트에 메서드 파라미터(id, username 등)와 authentication 객체가 바인딩된다
  4. 표현식의 결과가 true이면 실행, false면 AccessDeniedException이 발생됨

 

 

(2) 예시

// 메서드에 전달된 username과 현재 사용자의 이름이 같은지 검사함
@PreAuthorize("#username == authentication.name")
public void changePassword(String username, String newPassword) {
	
}

// 전달된 객체의 속성을 조건으로 활용하기
@PreAuthorize("#article.author == authentication.name")
public void editArticle(Article article) {

}

// 여러 조건 조합하기
@PreAuthorize("hasRole('ADMIN') or #userId == principal.id")
public void deleteAccount(Long userId) {

}

 

 

 

4. 정리 포인트

  • #파라미터명 : 메서드 인자 그대로 사용할 수 있음
  • authentication : SecurityContext의 Authentication 객체
  • principal : Authentication의 principal (보통 UserDetails)
  • 복잡한 조건 또한 and, or로 조합할 수 있음

 

 

5. 팁

  • API 단위 보안은 URL 기반, 비즈니스 로직 단위 보안은 메서드 보안을 적용하는 것이 일반적임
  • @PostAuthorize는 성능상 불리할 수 있기 때문에 꼭 필요한 경우에만 사용하기

 

 

6. 정리

애너테이션 설명
@PreAuthorize 메서드 실행 전 인가 검사
@PostAuthorize 메서드 실행 후 인가 검사
SpEL 파라미터 / 반환값 기반 동적 인가

인가 이벤트와 예외 처리

1. AuthorizationEvent 발행과 구독

인가 과정에서 이벤트가 발행되며, 이를 구독하여 로깅이나 모니터링을 수행할 수 있음

import org.springframework.context.event.EventListener;
import org.springframework.security.authorization.event.AuthorizationEvent;
import org.springframework.stereotype.Component;

@Component
public class CustomAuthorizationEventListener {
    @EventListener
    public void handleAuthEvent(AuthorizationEvent event) {
        System.out.println("인가 이벤트 발생 : " + event);
    }
}

 

 

2. 인가 실패시 예외 처리 전략

인가 실패시 AccessDeniedException이 발생함

기본적으로 403 Forbidden 응답이 반환된다

 

 

3. 커스텀 AccessDeniedHandler 구현하기

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.getWriter().write("접근 권한이 없습니다.");
    }
}

 

위의 핸들러를 Security 설정에 등록해서 커스텀 에러 메시지를 전달할 수 있다

 

 

  • 운영 환경에서는 인가 실패 로그를 반드시 기록해서 보안 사고를 추적할 수 있어야한다
  • AccessDeniedHandler를 커스터마이징하면 사용자 친화적인 에러 화면 제공이 가능해진다

 

정리

개념 설명
AuthorizationEvent 인가 이벤트 발행 / 구독
AccessDeniedException 인가 실패 예외
AccessDeniedHandler 인가 실패시 응답 처리