Let's say I have the following controller. (Assume that Order.customer is the customer the order belongs to and only they should be able to access it.)
#RestController
#RequestMapping("/orders")
public class OrderController {
#GetMapping
#PostAuthorize("returnObject.customer == authentication.principal")
public Order getOrderById(long id) {
/* Look up the order and return it */
}
}
After looking up the order, #PostAuthorize is used to make sure it belongs to the authenticated customer. If it is not, Spring responds with a 403 Forbidden.
Such an implementation has a problem: Clients can distinguish between orders that do not exist and orders they have no access to. Ideally, 404 should be returned in both cases.
While this could be solved by injecting the Authentication into the handler method and implementing custom logic there, is there any way to achieve this using #PostAuthorize or a similar, declarative API?
You can specify a custom AccessDeniedHandler in your Spring Security configuration.
In the following example, the handler will return a 404 Not Found on an access denied failure.
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler(accessDeniedHandler())
);
}
#Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
}
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value(), HttpStatus.NOT_FOUND.getReasonPhrase());
}
}
You could try a ControllerAdvice to catch and transform the AccessDeniedException, which PostAuthorize throws.
#RestControllerAdvice
public class ExceptionHandlerController {
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(AccessDeniedException.class)
public String handleAccessDenied(AccessDeniedException e) {
return "nothing here"; // or a proper object
}
}
I'm new to spring security so for learning purpouses I created a api using spring-boot 2.0.3.RELEASE and spring-boot-starter-security.
Here is what I got
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder().encode("admin")).roles("USER");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Controller class
#RestController
#RequestMapping(value = "/security")
public class AppController {
#Autowired
SpringSecurityService service;
#GetMapping(value = "/{id}")
public Person findOne(#PathVariable("id") final int id) {
return service.findOne(id);
}
}
When I try to consume the get method findOne via some browser a Login form is promped since all browsers support basic auth I try to login using admin as user and password but an error from wrong credentials is shown.
I tried also to consume the mehod via postman sending Authorization and $2a$10$c6MFPW.7MD7a.2V2rJYlXO0.YOLQEmsbu5GBmFsf.jShduBPenQ6O as the value.
I got the value for the password from here:
System.out.println(new BCryptPasswordEncoder().encode("admin"));
I know this is some rookie mistake but I dont know what I'm missing
Tried your code out in my own project. It would appear you're just missing an #Configuration annotation on your WebSecurityConfigurerAdapter thus the default configuration is taking place. Where the user name, and the logged password in the console would work.
Working example - https://github.com/DarrenForsythe/sof-60178305
#RestController
public class AccountController {
#PermitAll
#RequestMapping(value = "/test")
public ResponseEntity<String> test() {
// ...
}
#RolesAllowed("ROLE_ADMIN)
#RequestMapping(value = "/products")
public ResponseEntity<List<Product>> products() {
// ...
}
}
How to configure Spring Boot to be able to access "/test" without authentication, but "/products" with authentication and checking rights/roles?
Is it possible without mention paths of #PermitAll(like "/test") in configuration?
Question : Spring Boot to be able to access "/test" without authentication, but "/products" with authentication
Solution :
http.authorizeRequests().antMatchers("/test").permitAll()
.antMatchers("/products").hasRole("ADMIN").anyRequest().authenticated().and().httpBasic();
Note : By default when you add spring security, it ask authentication for all the url and you need to specify the one which you do not need authentication. For Example /login should be permitAll.
Click here for Source code of security configuration
Refer Sample HttpSecurity sample for more matchers example as below,
For more details : https://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html
You can do it providing the next configuration class. In this case everything is accessible, if not restricted by the annotations.
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().permitAll();
}
}
Check SecurityConfig class here for more configuration options.
I'm receiving a 404 error when accessing a particular page in my spring boot web application.
The strange thing is that I don't receive that error when the resource is mapped to a different location.
#RequestMapping(value="report", method = RequestMethod.GET)
public String getReportPage() {
return "templates/report.html";
}
works just fine while
#RequestMapping(value="report/{uuid}", method = RequestMethod.GET)
public String getReportPage() {
return "templates/report.html";
}
does not. I need the uuid parameter for my angular service so I cannot simply remove that from the path. I've tried adding the path variable to the model; that makes no difference.
The directory structure is set up as follows:
webapp
resources
...
templates
report.html
The configuration is pretty much an out of the box spring boot with some added resource handlers and some basic security:
#Configuration
public class MvcConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/resources/", "file:resources/");
}
}
#Configuration
#EnableWebSecurity
class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and()
.csrf().disable();
}
}
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
auth.authenticationProvider(authProvider());
}
.... custom user details service and authentication provider ...
}
Any thoughts about what may be causing this issue?
Edit: After some further investigation, it looks like anything mapped beyond the first level doesn't work for the web controller (but the rest controllers are working just fine). For example, a mapping with the value /web/report doesn't work either.
While looking through debug messages I found that the application was looking for the pages in the wrong place:
DEBUG : Looking up handler method for path /report/templates/report.html
Which is why only top level requests were working.
Changing the mapping as such:
#RequestMapping(value="report/{uuid}", method = RequestMethod.GET)
public String getReportPage() {
return "/templates/report.html";
}
fixed the problem.
I'm trying to figure out the simplest way to take control over the 404 Not Found handler of a basic Spring Boot RESTful service such as the example provided by Spring:
https://spring.io/guides/gs/rest-service/
Rather than have it return the default Json output:
{
"timestamp":1432047177086,
"status":404,
"error":"Not Found",
"exception":"org.springframework.web.servlet.NoHandlerFoundException",
"message":"No handler found for GET /aaa, ..."
}
I'd like to provide my own Json output.
By taking control of the DispatcherServlet and using DispatcherServlet#setThrowExceptionIfNoHandlerFound(true), I was able to make it throw an exception in case of a 404 but I can't handle that exception through a #ExceptionHandler, like I would for a MissingServletRequestParameterException. Any idea why?
Or is there a better approach than having a NoHandlerFoundException thrown and handled?
It works perfectly Fine.
When you are using SpringBoot, it does not handle (404 Not Found) explicitly; it uses WebMvc error response. If your Spring Boot should handle that exception, then you should do some hack around Spring Boot. For 404, the exception class is NoHandlerFoundException; if you want to handle that exception in your #RestControllerAdvice class, you must add #EnableWebMvc annotation in your Application class and set setThrowExceptionIfNoHandlerFound(true); in DispatcherServlet. Please refer to the following code:
#SpringBootApplication
#EnableWebMvc
public class Application {
#Autowired
private DispatcherServlet servlet;
public static void main(String[] args) throws FileNotFoundException, IOException {
SpringApplication.run(Application.class, args);
}
#Bean
public CommandLineRunner getCommandLineRunner(ApplicationContext context) {
servlet.setThrowExceptionIfNoHandlerFound(true);
return args -> {};
}
}
After this you can handle NoHandlerException in your #RestControllerAdvice class
#RestControllerAdvice
public class AppException {
#ExceptionHandler(value={NoHandlerFoundException.class})
#ResponseStatus(code=HttpStatus.BAD_REQUEST)
public ApiError badRequest(Exception e, HttpServletRequest request, HttpServletResponse response) {
e.printStackTrace();
return new ApiError(400, HttpStatus.BAD_REQUEST.getReasonPhrase());
}
}
I have created ApiError class to return customized error response
public class ApiError {
private int code;
private String message;
public ApiError(int code, String message) {
this.code = code;
this.message = message;
}
public ApiError() {
}
//getter & setter methods...
}
According to the Spring documentation appendix A. there is a boolean property called spring.mvc.throw-exception-if-no-handler-found which can be used to enable throwing NoHandlerFoundException. Then you can create exception handler like any other.
#RestControllerAdvice
class MyExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(MyExceptionHandler.class);
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(NoHandlerFoundException.class)
public String handleNoHandlerFoundException(NoHandlerFoundException ex) {
log.error("404 situation detected.",ex);
return "Specified path not found on this server";
}
}
#ExceptionHandler itself without #ControllerAdvice (or #RestControllerAdvice) can't be used, because it's bound to its controller only.
In short, the NoHandlerFoundException is thrown from the Container, not from your application within your container. Therefore your Container has no way of knowing about your #ExceptionHandler as that is a Spring feature, not anything from the container.
What you want is a HandlerExceptionResolver. I had the very same issue as you, have a look at my solution over there: How to intercept "global" 404s on embedded Tomcats in spring-boot
The #EnableWebMvc based solution can work, but it might break Spring boot auto configurations.
The solution I am using is to implement ErrorController:
#RestController
#RequiredArgsConstructor
public class MyErrorController implements ErrorController {
private static final String ERROR_PATH = "/error";
#NonNull
private final ErrorAttributes errorAttributes;
#RequestMapping(value = ERROR_PATH)
Map<String, Object> handleError(WebRequest request) {
return errorAttributes.getErrorAttributes(request, false);
}
#Override
public String getErrorPath() {
return ERROR_PATH;
}
}
The solution for this problem is:
Configure DispatcherServlet to throw and exception if it doesn't find any handlers.
Provide your implementation for the exception that will be thrown from DispatcherServlet, for this case is the NoHandlerFoundException.
Thus, in order to configure DispatcherServlet you may use properties file or Java code.
Example for properties.yaml,
spring:
mvc:
throw-exception-if-no-handler-found: true
Example for properties.properties,
spring.mvn.throw-exception-if-no-handler-found=true
Example for Java code, we just want to run the command servlet.setThrowExceptionIfNoHandlerFound(true); on startup, I use the InitializingBean interface, you may use another way. I found a very well written guide to run logic on startup in spring from baeldung.
#Component
public class WebConfig implements InitializingBean {
#Autowired
private DispatcherServlet servlet;
#Override
public void afterPropertiesSet() throws Exception {
servlet.setThrowExceptionIfNoHandlerFound(true);
}
}
Be careful! Adding #EnableWebMvc disables autoconfiguration in Spring Boot 2, meaning that if you use the annotation #EnableWebMvc then you should use the Java code example, because the spring.mvc.* properties will not have any effect.
After configuring the DispatcherServlet, you should override the ResponseEntityExceptionHandler which is called when an Exception is thrown. We want to override the action when the NoHandlerFoundException is thrown, like the following example.
#ControllerAdvice
public class MyApiExceptionHandler extends ResponseEntityExceptionHandler {
#Override
public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String responseBody = "{\"errormessage\":\"WHATEVER YOU LIKE\"}";
headers.add("Content-Type", "application/json;charset=utf-8");
return handleExceptionInternal(ex, responseBody, headers, HttpStatus.NOT_FOUND, request);
}
}
Finally, adding a break point to method handleException of ResponseEntityExceptionHandler might be helpful for debugging.