Skip to content

Commit

Permalink
Merge tag 'refs/tags/4.3.0'
Browse files Browse the repository at this point in the history
[maven-release-plugin] copy for tag 4.3.0
  • Loading branch information
wimdeblauwe committed Apr 30, 2024
2 parents a4c863f + a9e82db commit 0906862
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 91 deletions.
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</parent>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>error-handling-spring-boot-starter</artifactId>
<version>4.2.0</version>
<version>4.3.0</version>
<name>Error Handling Spring Boot Starter</name>
<description>Spring Boot starter that configures error handling</description>

Expand Down Expand Up @@ -50,7 +50,7 @@
</developerConnection>
<connection>scm:git:[email protected]:wimdeblauwe/error-handling-spring-boot-starter.git</connection>
<url>[email protected]:wimdeblauwe/error-handling-spring-boot-starter.git</url>
<tag>4.2.0</tag>
<tag>4.3.0</tag>
</scm>
<issueManagement>
<system>github</system>
Expand Down
84 changes: 83 additions & 1 deletion src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,9 @@ With this configuration, 400 Bad Request will be printed on DEBUG level.
401 Unauthorized will be printed on INFO.
Finally, all status code in the 5xx range will be printed on ERROR.

=== Spring Security AuthenticationEntryPoint
=== Spring Security

==== AuthenticationEntryPoint

By default, the library will not provide a response when there is an unauthorized exception.
It is impossible for this library to provide auto-configuration for this.
Expand Down Expand Up @@ -1239,6 +1241,75 @@ public class WebSecurityConfiguration {
<.> Define the UnauthorizedEntryPoint as a bean.
<.> Use the bean in the security configuration.

==== AccessDeniedHandler

Similar to the <<AuthenticationEntryPoint>>, there is also an `AccessDeniedHandler` implementation available at `io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponseAccessDeniedHandler`.

Example configuration:

[source,java]
----
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.UnauthorizedEntryPoint;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
public class WebSecurityConfiguration {
@Bean
public AccessDeniedHandler accessDeniedHandler(HttpStatusMapper httpStatusMapper,
ErrorCodeMapper errorCodeMapper,
ErrorMessageMapper errorMessageMapper,
ObjectMapper objectMapper) { //<.>
return new ApiErrorResponseAccessDeniedHandler(objectMapper, httpStatusMapper, errorCodeMapper, errorMessageMapper);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
AccessDeniedHandler accessDeniedHandler) throws Exception {
http.httpBasic().disable();
http.authorizeHttpRequests().anyRequest().authenticated();
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);//<.>
return http.build();
}
}
----

<.> Define the AccessDeniedHandler as a bean.
<.> Use the bean in the security configuration.

[NOTE]
====
You can perfectly combine the `AccessDeniedHandler` with the `UnauthorizedEntryPoint`:
[source,java]
----
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
UnauthorizedEntryPoint unauthorizedEntryPoint,
AccessDeniedHandler accessDeniedHandler) throws Exception {
http.httpBasic().disable();
http.authorizeHttpRequests().anyRequest().authenticated();
http.exceptionHandling()
.authenticationEntryPoint(unauthorizedEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
return http.build();
}
----
====

=== Handle non-rest controller exceptions

The library is setup in such a way that only exceptions coming from `@RestController` classes are handled.
Expand Down Expand Up @@ -1272,6 +1343,13 @@ public class FallbackExceptionHandler extends ErrorHandlingControllerAdvice {
}
----

=== Handle filter exceptions

By default, the library will not handle exceptions from custom filters.
Those are implementations of `jakarta.servlet.Filter`, usually subclasses of `org.springframework.web.filter.OncePerRequestFilter` in a Spring Boot application.

By setting the property `error.handling.handle-filter-chain-exceptions` to `true`, the library will handle those exceptions and return error responses just like is done for exceptions coming from controller methods.

== Custom exception handler

If the <<Configuration,extensive customization options>> are not enough, you can write your own `ApiExceptionHandler` implementation.
Expand Down Expand Up @@ -1395,6 +1473,10 @@ When this is set to `true`, you can use any superclass from your `Exception` typ
|error.handling.add-path-to-error
|This property allows to remove the `path` property in the error response when set to `false`.
|`true`

|error.handling.handle-filter-chain-exceptions
|Set this to `true` to have the library intercept any exception thrown from custom filters and also have the same error responses as exceptions thrown from controller methods.
|`false`.
|===

== Support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,30 @@
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;

import java.util.List;

public abstract class AbstractErrorHandlingConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractErrorHandlingConfiguration.class);

@Bean
@ConditionalOnMissingBean
public ErrorHandlingFacade errorHandlingFacade(List<ApiExceptionHandler> handlers,
FallbackApiExceptionHandler fallbackHandler,
LoggingService loggingService,
List<ApiErrorResponseCustomizer> responseCustomizers) {
handlers.sort(AnnotationAwareOrderComparator.INSTANCE);
LOGGER.info("Error Handling Spring Boot Starter active with {} handlers", handlers.size());
LOGGER.debug("Handlers: {}", handlers);

return new ErrorHandlingFacade(handlers, fallbackHandler, loggingService, responseCustomizers);
}

@Bean
@ConditionalOnMissingBean
public LoggingService loggingService(ErrorHandlingProperties properties) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
* Use this {@link AccessDeniedHandler} implementation if you want to have a consistent response
* with how this library works when the user is not allowed to access a resource.
* <p>
* It is impossible for the library to provide auto-configuration for this. So you need to manually add
* this to your security configuration. For example:
*
* <pre>
* public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {*
* &#64;Bean
* public AccessDeniedHandler accessDeniedHandler(HttpStatusMapper httpStatusMapper, ErrorCodeMapper errorCodeMapper, ErrorMessageMapper errorMessageMapper, ObjectMapper objectMapper) {
* return new ApiErrorResponseAccessDeniedHandler(objectMapper, httpStatusMapper, errorCodeMapper, errorMessageMapper);
* }
*
* &#64;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http,
* AccessDeniedHandler accessDeniedHandler) throws Exception {
* http.httpBasic().disable();
*
* http.authorizeHttpRequests().anyRequest().authenticated();
*
* http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
*
* return http.build();
* }
* }
* </pre>
*
* @see UnauthorizedEntryPoint
*/
public class ApiErrorResponseAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
private final HttpStatusMapper httpStatusMapper;
private final ErrorCodeMapper errorCodeMapper;
private final ErrorMessageMapper errorMessageMapper;

public ApiErrorResponseAccessDeniedHandler(ObjectMapper objectMapper, HttpStatusMapper httpStatusMapper, ErrorCodeMapper errorCodeMapper,
ErrorMessageMapper errorMessageMapper) {
this.objectMapper = objectMapper;
this.httpStatusMapper = httpStatusMapper;
this.errorCodeMapper = errorCodeMapper;
this.errorMessageMapper = errorMessageMapper;
}

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException {
ApiErrorResponse errorResponse = createResponse(accessDeniedException);

response.setStatus(errorResponse.getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}

public ApiErrorResponse createResponse(AccessDeniedException exception) {
HttpStatusCode httpStatus = httpStatusMapper.getHttpStatus(exception, HttpStatus.FORBIDDEN);
String code = errorCodeMapper.getErrorCode(exception);
String message = errorMessageMapper.getErrorMessage(exception);

return new ApiErrorResponse(httpStatus, code, message);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ErrorHandlingFacade {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorHandlingFacade.class);

private final List<ApiExceptionHandler> handlers;
private final FallbackApiExceptionHandler fallbackHandler;
private final LoggingService loggingService;
private final List<ApiErrorResponseCustomizer> responseCustomizers;

public ErrorHandlingFacade(List<ApiExceptionHandler> handlers, FallbackApiExceptionHandler fallbackHandler, LoggingService loggingService,
List<ApiErrorResponseCustomizer> responseCustomizers) {
this.handlers = handlers;
this.fallbackHandler = fallbackHandler;
this.loggingService = loggingService;
this.responseCustomizers = responseCustomizers;
}

public ApiErrorResponse handle(Throwable exception) {
ApiErrorResponse errorResponse = null;
for (ApiExceptionHandler handler : handlers) {
if (handler.canHandle(exception)) {
errorResponse = handler.handle(exception);
break;
}
}

if (errorResponse == null) {
errorResponse = fallbackHandler.handle(exception);
}

for (ApiErrorResponseCustomizer responseCustomizer : responseCustomizers) {
responseCustomizer.customize(errorResponse);
}

loggingService.logException(errorResponse, exception);

return errorResponse;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class ErrorHandlingProperties {

private boolean searchSuperClassHierarchy = false;

private boolean handleFilterChainExceptions = false;

public boolean isEnabled() {
return enabled;
}
Expand Down Expand Up @@ -143,6 +145,14 @@ public void setAddPathToError(boolean addPathToError) {
this.addPathToError = addPathToError;
}

public boolean isHandleFilterChainExceptions() {
return handleFilterChainExceptions;
}

public void setHandleFilterChainExceptions(boolean handleFilterChainExceptions) {
this.handleFilterChainExceptions = handleFilterChainExceptions;
}

public enum ExceptionLogging {
NO_LOGGING,
MESSAGE_ONLY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
* }
* }
* </pre>
*
* @see ApiErrorResponseAccessDeniedHandler
*/
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter.reactive;

import io.github.wimdeblauwe.errorhandlingspringbootstarter.*;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponse;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponseCustomizer;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiExceptionHandler;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ErrorHandlingFacade;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
Expand All @@ -13,30 +16,20 @@
import org.springframework.web.reactive.function.server.*;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Locale;

public class GlobalErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalErrorWebExceptionHandler.class);

private final List<ApiExceptionHandler> handlers;
private final FallbackApiExceptionHandler fallbackHandler;
private final LoggingService loggingService;
private final List<ApiErrorResponseCustomizer> responseCustomizers;
private final ErrorHandlingFacade errorHandlingFacade;

public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes,
WebProperties.Resources resources,
ErrorProperties errorProperties,
ApplicationContext applicationContext,
List<ApiExceptionHandler> handlers,
FallbackApiExceptionHandler fallbackHandler,
LoggingService loggingService,
List<ApiErrorResponseCustomizer> responseCustomizers) {
ErrorHandlingFacade errorHandlingFacade) {
super(errorAttributes, resources, errorProperties, applicationContext);
this.handlers = handlers;
this.fallbackHandler = fallbackHandler;
this.loggingService = loggingService;
this.responseCustomizers = responseCustomizers;
this.errorHandlingFacade = errorHandlingFacade;
}

@Override
Expand All @@ -55,23 +48,7 @@ public Mono<ServerResponse> handleException(ServerRequest request) {
LOGGER.debug("webRequest: {}", request);
LOGGER.debug("locale: {}", locale);

ApiErrorResponse errorResponse = null;
for (ApiExceptionHandler handler : handlers) {
if (handler.canHandle(exception)) {
errorResponse = handler.handle(exception);
break;
}
}

if (errorResponse == null) {
errorResponse = fallbackHandler.handle(exception);
}

for (ApiErrorResponseCustomizer responseCustomizer : responseCustomizers) {
responseCustomizer.customize(errorResponse);
}

loggingService.logException(errorResponse, exception);
ApiErrorResponse errorResponse = errorHandlingFacade.handle(exception);

return ServerResponse.status(errorResponse.getHttpStatus())
.contentType(MediaType.APPLICATION_JSON)
Expand Down
Loading

0 comments on commit 0906862

Please sign in to comment.