Backend/SpringBoot

[Spring Security] MethodSecurity 및 커스텀 핸들러

Dean83 2025. 12. 16. 14:24

SecurityFilterChain을 통해 url 에 대한 권한을 설정할 수 있는데, url으로는 권한체크가 어려운경우, 메소드 실행에서도 보안체크가 가능하다. 

 

사용에 있어 한가지 헷갈리는 경우는 보통 id와 연계되어 있는 경우는 인자값으로 받은 id 혹은 token에 있는 id를 이용하여 메소드에서 소유권 확인등이 가능한테 굳이 써야 하는가? 라는 의문이 들었다. 해당 처리 코드들이 중복되거나 누락될 수도 있기 때문에 "선택" 적으로 사용한다고 생각된다. 

 

사용법은 다음과 같다. 

  • @EnableMethodSecurity 어노테이션을, SecurityConfig Configuration에 붙여준다.
    • @EnableWebSecurity가 적용된곳에 같이 추가 하면 된다.
  • @PreAuthorize을 필요한 메소드에 각각 붙여 준다. (예 : Service 내 메소드들)
    • SpEL을 이용하여 표현식을 작성해 준다.
      • 메서드 파라메터 앞에는 #을 붙인다.
      • @를 이용하여 bean의 메소드를 호출 할 수도 있다.

 

별도의 권한을 확인하는 메소드가 bean으로 등록된 경우 

@Component
public class OrderPolicy {

    public boolean canCancel(Long orderId, Authentication auth) {
        Long userId = ((CustomUser) auth.getPrincipal()).getId();
        return isOwner(orderId, userId) || hasAdminRole(auth);
    }
}

@PreAuthorize("@orderPolicy.canCancel(#orderId, authentication)")
public void cancelOrder(Long orderId) {
    ...
}

 

 

메소드 인자값을 활용할 경우 (post의 body)

@PreAuthorize(
    "#request.userId == authentication.principal.id"
)
public void createOrder(CreateOrderRequest request) {
    ...
}

 

메소드인자값 및 기존 Role 동시 활용할 경우

@PreAuthorize(
    "hasRole('ADMIN') or #userId == authentication.principal.id"
)
public void deleteUser(Long userId) {
    ...
}

 

 

권한 인증 실패시 오류가 발생하면 다음과 같이 처리 해야 한다. 

  • ControllerAdvice 에서 AccessDeniedException을 잡지 않도록 한다.
    • 다른 AccessDeniedException의 경우 filter 에서 처리가 되는데, MethodSecurity만 ControllerAdvice에서 처리하는건 맞지 않다.
    • 만일 클라이언트와 오류 리턴양식을 정해 놓았다면, 커스텀 AccessDeniedHandler를 구현하여 Security 설정에 등록한다. 

 

커스텀 핸들러 예

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void handle(
        HttpServletRequest request,
        HttpServletResponse response,
        AccessDeniedException accessDeniedException
    ) throws IOException {

		//클라이언트와 논의한 양식에 맞게 작성
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=UTF-8");

        ErrorResponse error = ErrorResponse.builder()
            .code("ACCESS_DENIED")
            .message("접근 권한이 없습니다.")
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .build();

        response.getWriter().write(
            objectMapper.writeValueAsString(error)
        );
    }
}

 

커스텀 핸들러를 SecurityConfig에 추가

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	...


    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

     ...

        http
            .....

            .exceptionHandling(ex -> ex
                .accessDeniedHandler(accessDeniedHandler)
            )

            ......

        return http.build();
    }
}