Fails to work with Spring Security after upgrading to Spring Boot 2.5
See original GitHub issueproblem-spring-web
fails to work with Spring Security after upgrading to Spring Boot 2.5
Description
After introducing spring-security
as dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
problem-spring-web
will no longer work.
Expected Behavior
Work as expected.
For example, normally with problem-spring-web
, a validation failure message looks like below:
{
"title": "Bad Request",
"status": 400,
"detail": "Required request parameter 'message' for method parameter type String is not present"
}
Actual Behavior
-
Authentication failure will cause empty response with status 200.
-
Application will simply respond as Spring Boot’s default beheavior will do on other expections thrown:
{
"timestamp": "2021-11-10T00:00:00.000+00:00",
"status": 400,
"error": "Bad Request",
"path": "/echo"
}
Possible Fix
I personally have no idea about this
Steps to Reproduce
- Create a new project using Spring Initializr
- Include
problem-spring-web
as dependency:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web-starter</artifactId>
<version>0.27.0</version>
</dependency>
- Prepare a ValueObject
@Data(staticConstructor = "of")
@AllArgsConstructor
public class EchoMessage {
String message;
}
- Prepare a RestController
@RestController
public class EchoController {
@GetMapping(value = {"/echo", "/authorized/echo"})
public EchoMessage echo(@RequestParam @NotEmpty String message) {
return EchoMessage.of(message);
}
}
- Configure Spring security
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
return http
.authorizeRequests()
.antMatchers("/authorized/**")
.authenticated()
.anyRequest()
.anonymous()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable()
.httpBasic()
.disable()
.build();
// @formatter:on
}
}
- Write tests
@WebMvcTest
@ContextConfiguration(classes = SecurityTestConfiguration.class)
public class EchoControllerTest {
private final MockMvc mockMvc;
@Autowired
public EchoControllerTest(MockMvc mockMvc) {
this.mockMvc = mockMvc;
}
@Test
public void testEcho() throws Exception {
this.mockMvc.perform(get("/echo?message={message}", "Hello"))
.andExpect(status().is(200))
.andExpect(jsonPath("$.message", is("Hello")));
}
@Test
public void testEchoWithoutMessage() throws Exception {
this.mockMvc.perform(get("/echo"))
.andExpect(header().string("Content-Type", "application/problem+json"))
.andExpect(status().is(400))
.andExpect(jsonPath("$.title", is("Bad Request")))
.andExpect(jsonPath("$.status", is(400)));
}
@Test
public void testEchoPost() throws Exception {
this.mockMvc.perform(post("/echo?message={message}", "Hello"))
.andExpect(header().string("Content-Type", "application/problem+json"))
.andExpect(status().is(405))
.andExpect(jsonPath("$.title", is("Method Not Allowed")))
.andExpect(jsonPath("$.status", is(405)));
}
@Test
public void testAuthorizedEcho() throws Exception {
this.mockMvc.perform(get("/authorized/echo?message={message}", "Hello"))
.andExpect(header().string("Content-Type", "application/problem+json"))
.andExpect(status().is(401))
.andExpect(jsonPath("$.title", is("Unauthorized")))
.andExpect(jsonPath("$.status", is(401)));
}
}
All tests fails except for testEcho
.
I also tried simulating requests to /echo
using curl
, and got the same result.
Context
I find that if I register my own AdivceTrait bean in SecurityConfiguration
, problem-spring-web
will work again:
@Configuration
public class SecurityConfiguration {
@ControllerAdvice
public static class SecurityExceptionHandling implements ProblemHandling, SecurityAdviceTrait {}
@Bean
public AdviceTrait securityExceptionHandling() {
return new SecurityExceptionHandling();
}
// Other beans
}
Your Environment
- Spring Boot version: 2.5.6
problem-spring-web
version: 0.27.0- Java version: Adopt OpenJDK 11
AND here is the runnable demo project attached: demo.zip
Issue Analytics
- State:
- Created 2 years ago
- Reactions:5
- Comments:9
Top GitHub Comments
I think the root of the problem is introduced in this PR #413.
Before this change (in version 0.25.2 and below) there was
SpringSecurityExceptionHandling
bean that was directly annotated with@ControllerAdvice
and was imported usingspring.factories
.This way spring has registered it as a bean and also registered it as a
@ControllerAdvice
.The mechanism was changed in mentioned PR (0.26.0+).
Now, it’s
SecurityExceptionHandling
class that is also annotated with@ControllerAdvice
, but the subtle difference is that this class is not a bean (it’s marked as@Component
through annotation inheritance, but it’s not subject to package scan, so this mark is ignored). There is anotherProblemSecurityAutoConfiguration
class that registersSecurityExceptionHandling
as a bean. So far so good.But here is the problem. The bean type is
AdviceTrait
, that is not directly annotated with@ControllerAdvice
(and any parent of this type is also not annotated). This makes Spring to register it as a bean, but not as a@ControllerAdvice
. So, this is just a bean that sits in the container and does nothing (no-one calls it’s methods for any reason).If we change the return type of bean-factory method to
SecurityExceptionHandling
, the problem will be fixed. Spring will find@ControllerAdvice
annotation on the bean type and will register this advice, so all@ExceptionHandler
methods (including the method, that handlesThrowable
) will be registered.Basically, the code should be changed to this:
The PR #413 contains some tests, but they are not fully cover the case, e.g.:
This test checks that bean is present in the context and that it’s the correct implementation, but it doesn’t check that
@ControllerAdvice
is registered in Spring MVC (and it isn’t!).Also I found another test that should catch this problem, but it doesn’t. It doesn’t catch the problem because test class and advice both live in the same package, that falls under component auto-scan. I confirmed that by removing the code that registers
SecurityExceptionHandling
bean fromProblemSecurityAutoConfiguration
and the advice still works. The solution is to move all tests to the package, that will not catch main classes by auto-scan (e.g. into a sibling package). That would be a good thing to do as we are trying to test autoconfigurations here, but when they are subject to auto-scan, our test are flawed.As soon as I moved test classes into a sibling package the test started to fail, showing that
SecurityExceptionHandling
doesn’t work. It’s good, as now the test really does its job. And to make the test pass we could, e.g. change the return type of@Bean
method toSecurityExceptionHandling
instead ofAdviceTrait
, as I mentioned early.It’s interesting, that there is no problem with
ExceptionHandling
, that should be registered as@ControllerAdvice
when there is no spring-security in the context. It is registered fine although the return type is alsoAdviceTrait
here. I suppose it’s something with bean registration ordering and controller advice registration ordering. In some cases Spring can’t determine the exact type of the bean and looks on it’s declared type when tries to find@ControllerAdvice
annotation.We also depend on this bugfix. We have found a workaround for spring-problem-web v0.26.2 to just create an ExceptionHandling with ControllerAdvice ourselfs. Would be great if this bugfix could be released soon.