ASEP 어노테이션
ASEP 보안 예외 처리 경로를 위한 어노테이션 시스템입니다. @SecurityExceptionHandler 메서드 파라미터에 현재 사용자, 인증 객체, 요청 메타데이터를 주입하고, 일반적인 Spring MVC 예외 처리 이전의 필터 체인 수준에서 보안 실패를 처리합니다.
개요
ASEP 어노테이션은 Spring MVC의 DispatcherServlet 이전, Security Filter Chain 내에서 동작합니다. 이를 통해 인증 및 인가 실패를 Spring MVC 예외 처리 이전의 필터 수준에서 가로채어 처리할 수 있습니다.
ASEP Processing Flow
=====================
[HTTP Request]
|
v
[Security Filter Chain]
|
v
[ASEPFilter] --- on security exception ---> [@SecurityExceptionHandler]
| |
v v
[DispatcherServlet] [Custom Error Response]
|
v
[@SecurityPrincipal] - Injects current user
[@AuthenticationObject] - Injects authentication object
[@SecurityRequestHeader] - Injects request header
DSL에서 ASEP 활성화
각 인증 방식에 대해 asep()을 호출하여 ASEP 속성을 구성합니다.
registry
.form(form -> form
.loginPage("/login")
.asep(asep -> asep
.enableExceptionHandling(true)
.addArgumentResolver(customResolver)))
.session(Customizer.withDefaults())
.build();
ASEP 속성 클래스: FormAsepAttributes, RestAsepAttributes, OttAsepAttributes, PasskeyAsepAttributes, MfaAsepAttributes, MfaOttAsepAttributes, MfaPasskeyAsepAttributes
인자 주입 어노테이션
이 어노테이션들은 ASEP 핸들러 메서드 파라미터에 보안 컨텍스트 데이터를 직접 주입할 수 있게 합니다. 각각 대응하는 SecurityHandlerMethodArgumentResolver 구현체를 가지며, 일반적인 Spring MVC 컨트롤러 인자 주입 기능이 아니라 ASEP 런타임 내부에서 해석됩니다.
@SecurityPrincipal
현재 보안 주체(Principal)를 메서드 파라미터에 주입합니다. SecurityContextHolder에서 리졸브됩니다.
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
@SecurityExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthError(
@SecurityPrincipal Object principal,
AuthenticationException ex) {
// principal is resolved from SecurityContextHolder
return ResponseEntity.status(401).body(
new ErrorResponse("AUTH_FAILED", ex.getMessage()));
}
@AuthenticationObject
보안 컨텍스트에서 전체 Authentication 객체를 주입하여 자격 증명, 권한, 인증 세부 정보에 접근할 수 있게 합니다.
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
@SecurityExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(
@AuthenticationObject Authentication auth,
AccessDeniedException ex) {
String username = auth != null ? auth.getName() : "anonymous";
return ResponseEntity.status(403).body(
new ErrorResponse("ACCESS_DENIED", "User " + username + " lacks permission"));
}
@SecurityCookieValue
보안 예외 핸들러 컨텍스트 내에서 HTTP 요청의 쿠키 값을 주입합니다.
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value / name | String | "" | 쿠키 이름. |
required | boolean | true | 쿠키가 반드시 존재해야 하는지 여부. |
defaultValue | String | 없음 | 쿠키가 없고 필수가 아닌 경우의 기본값. |
@SecurityRequestBody
보안 예외 핸들러 컨텍스트 내에서 구성된 HttpMessageConverter 인스턴스를 사용하여 HTTP 요청 본문을 역직렬화합니다.
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
required | boolean | true | 요청 본문이 반드시 존재해야 하는지 여부. |
@SecurityRequestAttribute
서블릿 요청 속성 값을 주입합니다.
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value / name | String | "" | 요청 속성 이름. |
required | boolean | true | 속성이 반드시 존재해야 하는지 여부. |
@SecurityRequestHeader
HTTP 요청 헤더 값을 주입합니다.
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value / name | String | "" | 헤더 이름. |
required | boolean | true | 헤더가 반드시 존재해야 하는지 여부. |
defaultValue | String | 없음 | 헤더가 없고 필수가 아닌 경우의 기본값. |
@SecuritySessionAttribute
HTTP 세션 속성 값을 주입합니다.
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value / name | String | "" | 세션 속성 이름. |
required | boolean | true | 속성이 반드시 존재해야 하는지 여부. |
예외 처리 어노테이션
@SecurityExceptionHandler
메서드를 보안 예외 핸들러로 표시합니다. 이 어노테이션이 달린 메서드는 Security Filter Chain 내에서 일치하는 예외가 발생할 때 SecurityExceptionHandlerInvoker에 의해 호출됩니다. Spring MVC의 @ExceptionHandler에 대응하는 보안 계층 버전입니다.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | Class<? extends Throwable>[] | {} | 이 핸들러가 처리하는 예외 타입. 비어있으면 핸들러 메서드의 예외 파라미터 타입이 사용됩니다. |
priority | int | LOWEST_PRECEDENCE | 핸들러 우선순위. 낮은 값이 높은 우선순위를 가집니다. |
produces | String[] | {} | 생산 가능한 미디어 타입 (예: "application/json"). |
@SecurityControllerAdvice
클래스를 보안 컨트롤러 어드바이스 빈으로 표시하며, Spring MVC의 @ControllerAdvice에 대응합니다. 이 어노테이션이 달린 클래스는 SecurityExceptionHandlerMethodRegistry에 의해 스캔되어 @SecurityExceptionHandler 메서드를 탐색합니다.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value / basePackages | String[] | {} | 적용 범위를 위해 스캔할 기본 패키지. |
basePackageClasses | Class<?>[] | {} | 해당 패키지가 기본 패키지로 사용되는 클래스. |
assignableTypes | Class<?>[] | {} | 이 어드바이스가 적용되는 특정 타입. |
@SecurityControllerAdvice
public class GlobalSecurityExceptionAdvice {
@SecurityExceptionHandler(AuthenticationException.class)
@SecurityResponseBody
public ResponseEntity<Map<String, Object>> handleAuthFailure(
@AuthenticationObject Authentication auth,
@SecurityRequestHeader(value = "X-Request-ID", required = false) String requestId,
AuthenticationException ex) {
Map<String, Object> body = Map.of(
"error", "AUTHENTICATION_FAILED",
"message", ex.getMessage(),
"requestId", requestId != null ? requestId : "unknown"
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
}
@SecurityExceptionHandler(AccessDeniedException.class)
@SecurityResponseBody
public ResponseEntity<Map<String, Object>> handleAccessDenied(
@SecurityPrincipal Object principal,
AccessDeniedException ex) {
Map<String, Object> body = Map.of(
"error", "ACCESS_DENIED",
"message", ex.getMessage()
);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(body);
}
}
응답 어노테이션
@SecurityResponseBody
@SecurityExceptionHandler 메서드의 반환 값이 구성된 HttpMessageConverter 인스턴스를 사용하여 HTTP 응답 본문으로 직렬화되어야 함을 나타냅니다. 내부적으로 Spring의 @ResponseBody가 메타 어노테이션으로 포함되어 있습니다.
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME)
전체 예제
@SecurityControllerAdvice 와 ASEP 어노테이션을 사용한 보안 예외 처리 전체 예제입니다.
@SecurityControllerAdvice
public class GlobalSecurityExceptionHandler {
@SecurityExceptionHandler(AuthenticationException.class)
@SecurityResponseBody
public ErrorResponse handleAuthError(
AuthenticationException ex,
@AuthenticationObject Authentication auth,
@SecurityRequestHeader("X-Device-Id") String deviceId) {
String principalName = auth != null ? auth.getName() : "anonymous";
return new ErrorResponse("AUTH_FAILED", principalName + " on device " + deviceId);
}
@SecurityExceptionHandler(AccessDeniedException.class)
@SecurityResponseBody
public ErrorResponse handleAccessDenied(
AccessDeniedException ex,
@SecurityPrincipal Object principal,
@SecurityCookieValue("ctxa_trace") String traceId) {
return new ErrorResponse("ACCESS_DENIED", "principal=" + principal + ", trace=" + traceId);
}
}