Spring Method Security: Customize Error Message

In this tutorial, you will learn how to return a custom error message if the conditions of the Spring Method Security are not met.

To learn more about method-level security annotations read:

Controller class with Spring Method Security Annotation

Let’s assume that we have a REST Controller class that uses Spring Method-Level Security. In this controller class, we have a method secured with @PreAuthorize annotation.

The @PreAuthorize annotation requires that the user has an ADMIN role assigned to them. If this condition is not met, Spring Security will throw an error which will result in a 403 FORBIDDEN HTTP Status code returned to the client.

package com.appsdeveloperblog.photoapp.api.users;

import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UsersController {

    @DeleteMapping("/users/{id}")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String deleteUser(@PathVariable String id) {
        // Logic to delete user goes here
        return "User with id: " + id + " has been deleted";
    }
}

Where:

  • @RestController: This annotation marks the class as a controller where every method returns a domain object instead of a view. It’s shorthand for @Controller and @ResponseBody rolled together.
  • @DeleteMapping("/users/{id}"): This annotation maps HTTP DELETE requests to the deleteUser() method. The {id} in the URL is a path variable, which will be replaced with the actual ID of the user you want to delete.
  • @PreAuthorize("hasRole('ROLE_ADMIN')"): This annotation is a part of Spring Security. It specifies that the deleteUser() method can only be accessed by users who have the ‘ROLE_ADMIN’ role. This is a way of restricting access at the method level based on roles.

So in summary, this is a controller method that listens for HTTP DELETE requests on the “/users/{id}” URL, checks if the requester has the ‘ROLE_ADMIN’ role, and if so, deletes the user with the given ID and returns a confirmation message.

If the above method receives a request from a user that does not have an ADMIN role assigned to them, Spring Security will throw an exception. The goal of this tutorial is to learn how to handle this exception and return a custom error message. Let’s learn how to do it in the following section.

Handle Method Security Annotation Error and Customize it

So, what happens when the conditions of our Method Security annotation aren’t met? The Spring Framework isn’t just going to ignore it – instead, it’s going to throw an AccessDeniedException.

But there’s no need to worry. Our goal here is to gracefully handle this exception. We want to return a custom error message and a relevant HTTP status code instead of the default error response.

To accomplish this, we’re going to use two powerful tools provided by Spring: the @ControllerAdvice and @ExceptionHandler annotations. Let’s see how they can help us handle this scenario.

package com.appsdeveloperblog.photoapp.api.users.security;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class GlobalExceptionsHandler {
    @ExceptionHandler(value = { AccessDeniedException.class })
    public ResponseEntity<Object> handleAnyException(AccessDeniedException ex, WebRequest request) {
        return new ResponseEntity<>("My custom error message", new HttpHeaders(), HttpStatus.FORBIDDEN);
    }
}

Where:

  • @ControllerAdvice: This is a special annotation in Spring MVC that allows you to handle exceptions across your whole application, not just in an individual controller. You can think of it as an interceptor of exceptions thrown by methods annotated with @RequestMapping. With this annotation, you’re essentially saying “Hey, if there’s an exception, come here and I’ll take care of it.”
  • @ExceptionHandler(value = { AccessDeniedException.class }): This annotation is used to define the class of exception that the method should handle. In this case, it’s telling our application to use the handleAccessDeniedException() method whenever an AccessDeniedException is thrown.
  • So why AccessDeniedException? This exception is thrown when a user tries to access a resource they don’t have the right to access. For example, in a Spring Security application, if you’ve protected certain routes to be only accessible by administrators and a non-administrator tries to access it, `AccessDeniedException` will be thrown.
  • The handleAccessDeniedException() method: This method returns an instance of ResponseEntity. `ResponseEntity` represents the whole HTTP response, including the body, headers, and status code. In this method, we’re returning a custom error message as the body, HttpStatus.FORBIDDEN (HTTP status code 403) as the status code, and we’re creating an empty HttpHeaders instance (though you could add custom headers if you wanted).

You have the freedom here to include any custom error message and any custom HTTP status code. This gives you a lot of flexibility in controlling what your users see when an error occurs, which can greatly improve your application’s user experience. For example, you could return a helpful error message and a relevant status code to help your users understand what went wrong and what they can do next.

So, that’s the crux of it! This mechanism provides a neat, centralized place for handling specific types of exceptions in your Spring application, allowing you to provide consistent and helpful error responses to your users.

Final words

In this article, you learned how to customize the error message that is returned to the client when the conditions of the Spring Method Level Security are not met.

Some of the main takeaways of this article are:

  • Spring Method Level Security allows you to secure specific methods in your controller classes using annotations such as @PreAuthorize, @PostAuthorize, @Secured, and @RolesAllowed.
  • When a user tries to access a method that they are not authorized to, Spring Security will throw an AccessDeniedException.
  • To handle this exception and return a custom error message, you need to use the @ControllerAdvice and @ExceptionHandler annotations in another class.
  • The @ControllerAdvice annotation allows you to handle exceptions across your whole application, not just in an individual controller.
  • The @ExceptionHandler annotation allows you to define a method that handles a specific type of exception and returns a custom response.
  • You can create a custom error message object that contains the status, message, and error fields. You can customize it according to your needs.
  • You can use the ResponseEntity class to return the error message object as the response body with the appropriate status code.

I hope this article was helpful and informative for you. If you are interested to learn more about Spring Security, please check out other Spring Security tutorials.