- 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는 권한 부여 처리를 총괄하는 매니저 역할을 하는 인터페이스이고 RequestMatcherDelegatingAuthorizationManager는 AuthorizationManager를 구현하는 구현체중 하나이다
- RequestMatcherDelegatingAuthorizationManager는 RequestMatcher 평가식을 기반으로 해당 평가식에 매치되는 AuthorizationManager에게 권한 부여 처리를 위임하는 역할을 한다
- RequestMatcherDelegatingAuthorizationManager가 직접 권한 부여 처리를 하는게 아니라,
RequestMatcher를 통해 매치되는 AuthorizationManager 구현 클래스에게 위임만 한다는 사실을 꼭 기억해야함
- RequestMatcherDelegatingAuthorizationManager가 직접 권한 부여 처리를 하는게 아니라,
- RequestMatcherDelegatingAuthorizationManager 내부에서 매치되는 AuthorizationManager 구현 클래스가 있다면
해당 AuthorizationManager 구현 클래스가 사용자의 권한을 체크한다 (3) - 적절한 권한이라면, 다음 요청 프로세스를 계속 이어간다 (4)
- 만약 적절한 권한이 아니라면 AccessDeniedException이 throw되고 ExceptionTranslationFilter가 AccessDeniedException을 처리하게 된다 (5)
인가 처리 흐름에서 사용되는 컴포넌트의 세부 흐름
1. FilterSecurityInterceptor와 AuthorizationFilter
SpringSecurity의 인가 과정은 보통 필터체인에서 수행된다
주요 필터
- FilterSecurityInterceptor : 가장 전통적인 인가 처리 필터로, URL 요청에 대한 접근 권한을 검사한다
- AuthorizationFilter : Spring Security 5.5+ 에서 추가된 새로운 필터로, AuthorizationManager와 함께 동작한다
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는 위에서 아래로 규칙을 평가하며, 먼저 매칭된 규칙이 최종적으로 적용된다
구체적인 경로를 먼저 선언하고, 일반적인 규칙은 아래에 배치하는 것이 안전하다.
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) 동작 원리
- 스프링 AOP가 메서드의 호출을 가로챔
- 메서드 실행 전, @PreAuthorize 안의 SpEL 표현식을 평가한다
- SpEL 컨텍스트에 메서드 파라미터(id, username 등)와 authentication 객체가 바인딩된다
- 표현식의 결과가 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 | 인가 실패시 응답 처리 |
'Spring Boot > Security' 카테고리의 다른 글
| 커스텀 필터 구현 : 필요성 (0) | 2026.04.29 |
|---|---|
| * 인가 아키텍처 : 핵심 컴포넌트 * (0) | 2026.04.29 |
| * 인증 아키텍처 : 핵심 컴포넌트 * (0) | 2026.04.24 |
| 필터 아키텍처 : 필터 순서와 우선순위 (1) | 2026.04.21 |
| 필터 아키텍처 : 주요 보안 필터 (0) | 2026.03.31 |