Expected Behavior
The AccessDeniedHandler should be able to handle all AccessDeniedException.
Current Behavior
The AccessDeniedException thrown from the AuthorizationManagerBeforeMethodInterceptor does not appear to have been processed by the ExceptionTranslationFilter. Is this normal, a point that needs enhancement, or a bug? I am not sure. Please help me with my doubts, thank you.
Context
As shown in the figure, I correctly configured the AccessDeniedHandler, but the AccessDeniedException thrown due to using @ PreAuthorize will not be processed by it.
Comment From: jzheaux
Hi, @insight720. Instead of a screenshot, will you please provide a minimal sample application? This will help get your issue addressed faster.
Comment From: insight720
Sure. @jzheaux
My application context info
Oracle JDK 17
Window 11
Spring Boot 3.0.3 (which includes Spring Security 6.0.2)
Key Code
- Simplified
SpringSecurityConfig
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // use Method Security
public class SpringSecurityConfig {
@Configuration
@RequiredArgsConstructor
public static class SecurityFilterChainConfig {
// autowired
private final AccessDeniedHandler accessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
// accessDeniedHandler is configured
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
return http.build();
}
}
- Normal
AccessDeniedHandlerImpl
java
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) {
// nothing important
}
}
- Use of
@PreAuthorize
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@PutMapping("/authority")
public Result<Void> modifyAccountAuthority(@Valid @RequestBody AccountAuthorityDTO accountAuthorityDTO) {
userAccountService.updateAccountAuthority(accountAuthorityDTO);
return ResultUtils.success();
}
- A user without
ROLE_ADMINauthority requests this method, then anAccessDeniedExceptionis thrown by Spring Security, but can't catch byAccessDeniedHandlerImpl.
My question is, is this situation normal? Or is this a BUG?
Comment From: imaxkhan
hi i have exactly same problem. is there any way?
Comment From: insight720
@imaxkhan I am waiting for reply, and I have not further studied this issue at the moment. You can try whether @ControllerAdvice can catch exceptions and handle them, but I haven't tried that before. @jzheaux Could you please take a look at the question raised by this issue again? Thank you very much!
Comment From: Drophoff
Hi,
i have the same failure with Spring Boot 3.0.5 and Spring Security 6.0.2.
Yes, the exception can be handled with @ControllerAdvice, but we cannot distinguish between authentication or authorization errors, which is the case with the AccessDeniedHandler.
Furthermore the API to register an dedicated AccessDeniedHandler within the HttpSecurity is not working as documented and the ExceptionTranslationFilter, which has the aim to handle this kind of failure gets never called.
Comment From: HabeebCycle
Are we triaging this issue? Spring Security 6.0.2
Comment From: Drophoff
I updated the dependencies to Spring Boot 3.1.2 and Spring Security 6.1.2 and the failure is still present.
Comment From: marcusdacoregio
Hi everyone, if you clone my sample and perform the request http :8080/ -a user:password or if I open the browser and provide the user:password credentials, I get the proper 418 status code. Can you elaborate more on how to simulate the problem? Feel free to use my sample.
Comment From: Drophoff
I compared my configuration with the above provided one and found a configuration failure on my side. I would like to apologize for the inconvenience.
I have defined a @ControlledAdvice, which handled the AccessDeniedException and due to that the exception never reached the DispacherServlet nor the ExceptionTranslationFilter.
Thus, my error message is invalid and no longer valid.
Comment From: jzheaux
Thanks for the update, @Drophoff, and I'm glad you and @marcusdacoregio were able to sort things out.
Comment From: Rei-Nicolau-o-Grande
I imported the exceptions from Spring Security and it worked.
@ExceptionHandler({
org.springframework.security.authorization.AuthorizationDeniedException.class, // Import Spring Security
org.springframework.security.access.AccessDeniedException.class, // Import Spring Security
AccessDeniedException.class, // My Custom Excepition
})
public ResponseEntity<ApiErrorDto> handlerForbiddenException(HttpServletRequest request,
RuntimeException ex) {
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.contentType(MediaType.APPLICATION_JSON)
.body(new ApiErrorDto(
LocalDateTime.now(),
request.getRequestURI(),
request.getMethod(),
HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase(),
ex.getMessage(),
null,
ex.getClass()
));
}
SecurityConfig does not need exceptionHandling to work.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Value("${jwt.public.key}")
private RSAPublicKey publicKey;
@Value("${jwt.private.key}")
private RSAPrivateKey privateKey;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.POST, "/api/v1/token/login").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/users").permitAll()
.requestMatchers(DOCUMENTATION_OPENAPI).permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable())
.oauth2ResourceServer(
oauth2 -> oauth2.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtMyAuthenticationConverter())))
.sessionManagement(
session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// .exceptionHandling(
// exceptionHandling -> exceptionHandling
// .accessDeniedHandler(new CustomAccessDeniedHandler()))
;
return http.build();
}
Response
Comment From: Rei-Nicolau-o-Grande
Now if you want to customize the response error message.
My DTO
public record ApiErrorDto(
@JsonFormat(pattern="dd-MM-yyyy HH:mm:ss")
LocalDateTime timestamp,
String path,
String method,
Integer status,
String error,
String message,
@JsonInclude(JsonInclude.Include.NON_NULL)
Map<String, String> fields,
@JsonInclude(JsonInclude.Include.NON_NULL)
Object stakeTrace
) {
public ApiErrorDto(LocalDateTime timestamp, String path, String method, Integer status, String error,
String message) {
this(timestamp, path, method, status, error, message, null, null);
}
public ApiErrorDto(LocalDateTime timestamp, String path, String method, Integer status, String error,
String message, Map<String, String> fields, Object stakeTrace) {
this.timestamp = timestamp;
this.path = path;
this.method = method;
this.status = status;
this.error = error;
this.message = message;
this.fields = fields;
this.stakeTrace = stakeTrace;
}
}
Create the CustomAccessDeniedHandler class and implement import org.springframework.security.web.access.AccessDeniedHandler;
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
public CustomAccessDeniedHandler() {
this.objectMapper = new ObjectMapper();
this.objectMapper.registerModule(new JavaTimeModule());
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// response.sendError(HttpServletResponse.SC_FORBIDDEN, "Acesso negado! component customizado | "
// + accessDeniedException.getMessage());
ApiErrorDto apiErrorDto = new ApiErrorDto(
LocalDateTime.now(),
request.getRequestURI(),
request.getMethod(),
HttpServletResponse.SC_FORBIDDEN,
HttpStatus.FORBIDDEN.getReasonPhrase(),
"Acesso negado! CustomAccessDeniedHandler",
null,
null
);
response.getWriter().write(objectMapper.writeValueAsString(apiErrorDto));
}
}
Now, in SecurityConfig, you need to add exceptionHandling.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Value("${jwt.public.key}")
private RSAPublicKey publicKey;
@Value("${jwt.private.key}")
private RSAPrivateKey privateKey;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.POST, "/api/v1/token/login").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/users").permitAll()
.requestMatchers(DOCUMENTATION_OPENAPI).permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable())
.oauth2ResourceServer(
oauth2 -> oauth2.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtMyAuthenticationConverter())))
.sessionManagement(
session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(
exceptionHandling -> exceptionHandling
.accessDeniedHandler(new CustomAccessDeniedHandler()))
;
return http.build();
}
Now ExceptionHandler removes AccessDeniedException.class and AuthorizationDeniedException.class.
@ExceptionHandler({
AccessDeniedException.class, // My Exception
})
public ResponseEntity<ApiErrorDto> handlerForbiddenException(HttpServletRequest request,
RuntimeException ex) {
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.contentType(MediaType.APPLICATION_JSON)
.body(new ApiErrorDto(
LocalDateTime.now(),
request.getRequestURI(),
request.getMethod(),
HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase(),
ex.getMessage(),
null,
ex.getClass()
));
}
Response