Describe the bug When the user is not logged in and the browser accesses the protected resource, the browser will jump to the login page. If the browser automatically visits a nonexistent resource (e.g., favicon.ico) on the login page, the browser will automatically navigate to the /error path after login.
To Reproduce Create a Spring Boot Project with dependencies 'org.springframework.boot:spring-boot-starter-web' and 'org.springframework.boot:spring-boot-starter-security'. In the security configuration, a form login is provided and requires login to access all paths except the /favicon.ico path, which allows an anonymous login. The most important thing is that there is no favicon.ico in this project. Then visit 127.0.0.1:8080/hello or other secured resources in your browser. The browser will be redirect to /login. Enter username and password(admin:admin) and click the login button. The browser will be redirected to /error instead of /hello.
Expected behavior After logged in, the browser redirects to the url before /login or home page instead of /error.
Sample Here's the contents of the application.yml file. It provides a user with a username and password of admin.
spring:
security:
user:
name: admin
password: admin
````
Here are the security configuration classes. It's configured for form login. It allows anonymous access to /favicon.ico and requires a login for all other paths.
```Java
package example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults());
http.authorizeRequests(authorize -> authorize
.requestMatchers("/favicon.ico").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
A controller.
package org.anonym.authorizationserver.conntroller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
}
Analyse org.springframework.security.web.savedrequest.HttpSessionRequestCache#saveRequest(HttpServletRequest, HttpServletResponse) method is the key to this problem. When an anonymous user accesses a protected resource, the backend will store the path to the protected resource in the RequestCache and instruct the browser to navigate to the /login path. Some request paths the RequestCache will not save are /favicon.ico requests, XHR requests, JSON requests, and file upload requests. When the browser navigates to /login, it will automatically request /favicon.ico for a website icon that doesn't exist in the application. The application then forwards the request to the /error path. Since the /error path requires a login to access it, the /error path is stored in the RequestCache instead of the pre-login path. When the user logs in, the application fetches the saved /error path from the RequestCache and tells the browser to jump to it.
Improvement The value of the Sec-Fetch-Dest header for active requests initiated by the browser is document, and the Sec-Fetch-Dest header for non-active requests is not document. Also, the dispatcherType value forwarded to the /error path is ERROR. The above two conditions can be used to determine whether the request path should be saved. The change to HttpSessionRequestCache is in the public void saveRequest(HttpServletRequest, HttpServletResponse) method.
package org.springframework.security.web.savedrequest;
// other code...
public class HttpSessionRequestCache implements RequestCache {
// other code...
@Override
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
boolean activeRequest = "document".equals(request.getHeader("Sec-Fetch-Dest"));
boolean dispatchToError = DispatcherType.ERROR.equals(request.getDispatcherType());
if ((!activeRequest && dispatchToError) || !this.requestMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not save request since it did not match [%s]", this.requestMatcher));
}
return;
}
// other code...
}
// other code...
}