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.
Related
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
In Spring MVC with Spring Security, is it possible to achieve this?
#Override WebSecurityConfigurerAdapter.configure(HttpSecurity)
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.mvcMatchers("/users/{authentication.principal.username}").hasAnyRole(ADMIN, MANAGER)
.antMatchers("/users/**").hasRole(ADMIN)
.anyRequest().authorized()
...
}
/users/** is a restricted area and should be accessible by admins only. But managers should still be able to see their own profile (/users/user_with_manager_role), and only their own profile, not those of any other users (regardless of their role).
Solution
I've found a solution in Andrew's answer. My Code now looks like this:
WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true) // added this annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter
#Override WebSecurityConfigurerAdapter.configure(HttpSecurity)
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
// removed /users handling
.anyRequest().authorized()
...
}
UsersController
#Controller
#RequestMapping("/users")
public class UsersController
{
#GetMapping("{username}")
#PreAuthorize("authentication.principal.username == #username) || hasRole('ADMIN')")
public String usersGet(#PathVariable("username") String username)
{
// do something with username, for example get a User object from a JPA repository
return "user";
}
}
I'm afraid it's not possible: when this configuration is being set up, it has no info about {authentication.principal.username} which will be resolved at some point in future.
But Spring gives you a bunch of built-in method security expressions you can annotate your methods with.
Starting from a simple expression like #PreAuthorize("hasRole('ADMIN')"), you might end up with a custom one:
#XMapping(path = "/users/{username}")
#PreAuthorize("#yourSecurityService.isMyPage(authentication.principal, #username)")
public void yourControllerMethod(#PathVariable String username);
#yourSecurityService.isMyPage(authentication.principal, #username) refers to your #Service method public boolean isMyPage(Principal, String).
How about something like this:
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/myself").hasAnyRole(ADMIN, MANAGER)
.antMatchers("/users/**").hasRole(ADMIN)
.anyRequest().hasAnyRole(ADMIN, MANAGER)
...
}
#RequestMapping(value = "/myself", method = RequestMethod.GET)
public Profile getMyself() {
// return the profile of the loged in user
}
With this manager and admins can get their own profile and admins can also request other profiles with /users/{username}.
#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've been building a REST API using Spring. I'm using Basic Authentication with Spring Security (3.2) and I'm having an issue where any unauthenticated request causes a 404 error, even with an implementation of AuthenticationEntryPoint (regardless, Spring should give a 401 as far as I am aware by default). Requesting the resource in my browser, I am not even prompted for credentials. Here's a screenshot of the problem:
After reading the documentation and a number of tutorials on the subject, I can't seem to find where I've gone wrong. The only thing I can imagine is happening is some exception that's being caught.
Spring Security configuration:
#Slf4j
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String REALM_NAME = "Autopulse API";
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// Set to stateless authentication.
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.csrf().disable();
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
httpSecurity.userDetailsService(userDetailsService);
httpSecurity.authorizeRequests()
.antMatchers("/make/private").authenticated();
httpSecurity.httpBasic().realmName(REALM_NAME);
}
}
Authentication Entry Point:
#Slf4j
#Component
public class HttpBasicAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
Controller:
#Slf4j
#RestController
#RequestMapping("/make")
public class MakeController {
#RequestMapping(value = "/private", method = RequestMethod.GET)
public String getPrivateStuff() {
return "private things!";
}
}
When I provide valid user credentials in the Authorization header, I can see the protected resource ("private things!"), however if I do not provide an Authorization header, or I enter invalid credentials, I simply get the 404 error. I can attach my user details service and user details classes if required.
I figured it out. The problem came down to Spring and exception handling. I had a class called ExceptionController that looked like:
#Slf4j
#RestController
#ControllerAdvice
public class ExceptionController implements ErrorController {
// #ExceptionHandler methods here.
#Override
public String getErrorPath() {
return null;
}
}
It turns out that, by implementing ErrorController, I was handing control to this class AFTER the authentication entry point where Spring could not find the appropriate method and would eventually throw the unwanted 404 error. The fix was to remove those details:
#Slf4j
#RestController
#ControllerAdvice
public class ExceptionController {
// #ExceptionHandler methods here.
}