使用@WithMockUser 的 Rest Assured 测试适用于 GET 请求,但不适用于 POST(403 错误)
Rest Assured tests using @WithMockUser work for GET requests, but not POST (403 Error)
在我的 Spring Boot 2 应用程序中,我使用 Rest Assured 来测试我的一个控制器。此控制器在 class 级别映射为 /routing-status
并具有 3 个端点:一个 GET all、一个 GET by ID 和一个 POST.
我为 GET 端点进行的 Rest Assured 测试工作得很好,但出于某种原因,POST 测试一直导致 403 错误。 (java.lang.AssertionError: 1 expectation failed.
Expected status code <201> but was <403>.
)我不明白为什么,考虑到整个控制器的安全性是相同的,而且我的测试对每个端点使用相同的模拟用户配置。
也许是其他安全设置问题只影响 POSTs(或者至少不影响 GET)?下面是我当前的安全配置,尽管我一直在弄乱端点通配符(无济于事)以查看没有尾部斜杠是否有效。
httpSecurity
.csrf().csrfTokenRepository(csrfTokenRepository)
.and().cors()
.and().addFilter(createCasAuthFilter())
.headers().frameOptions().disable()
.and().exceptionHandling().authenticationEntryPoint(noRedirectAuthenticationEntryPoint)
.and().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/login/redirect").authenticated()
.antMatchers("/login/**", "/status/**", "/error/**", "/smoke-test/**", LOGOUT_PATH).permitAll()
// BELOW IS THE CONTROLLER ENDPOINT IN QUESTION
.antMatchers("/routing-status/**").hasRole("ROUTING_STATUS_ADMIN")
// ABOVE IS THE CONTROLLER ENDPOINT IN QUESTION
.antMatchers(HttpMethod.GET, "/user/**").authenticated()
.antMatchers("/hello-user**", "/exit-user**", "/roles-map**").authenticated()
.anyRequest().denyAll().and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH))
.logoutSuccessUrl(casUrl + LOGOUT_PATH)
.and().authorizeRequests();
下面是我失败的 POST 放心测试。 GET 请求都使用具有相同常量值的完全相同的 @WithMockUser
注释,并且它们都命中相同的根端点 /routing-status
(尽管在 GET by ID 的情况下,额外的 /{ID}
通过)。我尝试在 POST URL 中包含尾部斜杠并保留尾部斜杠,但没有成功。
private static final String ADMIN_ROLE = "ROUTING_STATUS_ADMIN";
/* Other fields and methods omitted */
@Test
@WithMockUser(username = TEST_USERNAME, roles = {ADMIN_ROLE})
public void testInsertRoutingStatus() {
RoutingStatus status = createRoutingStatus();
RoutingStatus result = given().mockMvc(mockMvc).contentType(ContentType.JSON).log().all()
.and().body(status)
.when().post("/routing-status/")
.then().log().all().statusCode(201)
.and().body(matchesJsonSchemaInClasspath("json-schemas/routing-status.json"))
.extract().as(RoutingStatus.class);
}
为了更好地衡量,这里是控制器的相关部分:
@Controller
@RequestMapping(value = "/routing-status", produces = MediaType.APPLICATION_JSON_VALUE)
public class RoutingStatusController {
/* Other properties and endpoints omitted */
@PostMapping(value = "")
public ResponseEntity<RoutingStatus> insertRoutingStatus(
@Valid @RequestBody final RoutingStatus routingStatus, final Principal principal) {
return new ResponseEntity<>(routingStatusService.saveNewRoutingStatus(routingStatus, principal.getName()),
HttpStatus.CREATED);
}
}
当为 spring 安全启用调试日志记录时(使用 logging.level.org.springframework.security=DEBUG
属性),错误前的输出是(时间戳,日志级别 [DEBUG],完全合格的 class 名称 [所有 spring boot/security 包],为简洁起见,删除了其他一些不相关的细节:
HttpSessionSecurityContextRepository:174 - No HttpSession currently exists
HttpSessionSecurityContextRepository:116 - No SecurityContext was available from the HttpSession: null. A new one will be created.
HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper:423 - HttpSession being created as SecurityContext is non-default
HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper:377 - SecurityContext 'SecurityContextImpl@6ea1035d: Authentication: UsernamePasswordAuthenticationToken@6ea1035d: Principal: User@e85371e3: Username: TEST_USERNAME; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ROUTING_STATUS_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_ROUTING_STATUS_ADMIN' stored to HttpSession: 'MockHttpSession@294ab038
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 1 of 13 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 2 of 13 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
HttpSessionSecurityContextRepository:207 - Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: 'SecurityContextImpl@6ea1035d: Authentication: UsernamePasswordAuthenticationToken@6ea1035d: Principal: User@e85371e3: Username: TEST_USERNAME; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ROUTING_STATUS_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_ROUTING_STATUS_ADMIN'
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 3 of 13 in additional filter chain; firing Filter: 'HeaderWriterFilter'
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 4 of 13 in additional filter chain; firing Filter: 'CorsFilter'
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 5 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
CsrfFilter:110 - Invalid CSRF token found for http://localhost/routing-status/
HstsHeaderWriter:129 - Not injecting HSTS header since it did not match the requestMatcher HstsHeaderWriter$SecureRequestMatcher@247415be
SecurityContextPersistenceFilter:119 - SecurityContextHolder now cleared, as request processing completed
403 Forbidden
所以 csrf 似乎是个问题?但是,当应用程序为 运行 时,csrf 工作正常,我不确定它为什么要检查 http://localhost/routing-status
。当 运行 在本地时,在 localhost
和 /routing-status
之间有一个端口号和一个上下文路径。
我得到 403 的原因是因为测试中没有 CSRF 令牌。我对 CSRF 不是很熟悉,但是根据我目前的发现,它不会影响 GET 请求,因为 CSRF 只影响像 POST 和 PUT 这样的写请求。我仍在努力将 CSRF 步骤添加到 RestAssured 测试中,但至少我现在知道了该行为的原因。我打算按照 Rest Assured 的 github 页面上的说明完全解决问题:https://github.com/rest-assured/rest-assured/wiki/usage#csrf
这个问题的完美解决方案如下:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class TestClass {
@Autowired
private WebApplicationContext webApplicationContext;
@BeforeEach
public void setUp() {
RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
RestAssuredMockMvc.postProcessors(csrf().asHeader());
}
}
在我的 Spring Boot 2 应用程序中,我使用 Rest Assured 来测试我的一个控制器。此控制器在 class 级别映射为 /routing-status
并具有 3 个端点:一个 GET all、一个 GET by ID 和一个 POST.
我为 GET 端点进行的 Rest Assured 测试工作得很好,但出于某种原因,POST 测试一直导致 403 错误。 (java.lang.AssertionError: 1 expectation failed.
Expected status code <201> but was <403>.
)我不明白为什么,考虑到整个控制器的安全性是相同的,而且我的测试对每个端点使用相同的模拟用户配置。
也许是其他安全设置问题只影响 POSTs(或者至少不影响 GET)?下面是我当前的安全配置,尽管我一直在弄乱端点通配符(无济于事)以查看没有尾部斜杠是否有效。
httpSecurity
.csrf().csrfTokenRepository(csrfTokenRepository)
.and().cors()
.and().addFilter(createCasAuthFilter())
.headers().frameOptions().disable()
.and().exceptionHandling().authenticationEntryPoint(noRedirectAuthenticationEntryPoint)
.and().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/login/redirect").authenticated()
.antMatchers("/login/**", "/status/**", "/error/**", "/smoke-test/**", LOGOUT_PATH).permitAll()
// BELOW IS THE CONTROLLER ENDPOINT IN QUESTION
.antMatchers("/routing-status/**").hasRole("ROUTING_STATUS_ADMIN")
// ABOVE IS THE CONTROLLER ENDPOINT IN QUESTION
.antMatchers(HttpMethod.GET, "/user/**").authenticated()
.antMatchers("/hello-user**", "/exit-user**", "/roles-map**").authenticated()
.anyRequest().denyAll().and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH))
.logoutSuccessUrl(casUrl + LOGOUT_PATH)
.and().authorizeRequests();
下面是我失败的 POST 放心测试。 GET 请求都使用具有相同常量值的完全相同的 @WithMockUser
注释,并且它们都命中相同的根端点 /routing-status
(尽管在 GET by ID 的情况下,额外的 /{ID}
通过)。我尝试在 POST URL 中包含尾部斜杠并保留尾部斜杠,但没有成功。
private static final String ADMIN_ROLE = "ROUTING_STATUS_ADMIN";
/* Other fields and methods omitted */
@Test
@WithMockUser(username = TEST_USERNAME, roles = {ADMIN_ROLE})
public void testInsertRoutingStatus() {
RoutingStatus status = createRoutingStatus();
RoutingStatus result = given().mockMvc(mockMvc).contentType(ContentType.JSON).log().all()
.and().body(status)
.when().post("/routing-status/")
.then().log().all().statusCode(201)
.and().body(matchesJsonSchemaInClasspath("json-schemas/routing-status.json"))
.extract().as(RoutingStatus.class);
}
为了更好地衡量,这里是控制器的相关部分:
@Controller
@RequestMapping(value = "/routing-status", produces = MediaType.APPLICATION_JSON_VALUE)
public class RoutingStatusController {
/* Other properties and endpoints omitted */
@PostMapping(value = "")
public ResponseEntity<RoutingStatus> insertRoutingStatus(
@Valid @RequestBody final RoutingStatus routingStatus, final Principal principal) {
return new ResponseEntity<>(routingStatusService.saveNewRoutingStatus(routingStatus, principal.getName()),
HttpStatus.CREATED);
}
}
当为 spring 安全启用调试日志记录时(使用 logging.level.org.springframework.security=DEBUG
属性),错误前的输出是(时间戳,日志级别 [DEBUG],完全合格的 class 名称 [所有 spring boot/security 包],为简洁起见,删除了其他一些不相关的细节:
HttpSessionSecurityContextRepository:174 - No HttpSession currently exists
HttpSessionSecurityContextRepository:116 - No SecurityContext was available from the HttpSession: null. A new one will be created.
HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper:423 - HttpSession being created as SecurityContext is non-default
HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper:377 - SecurityContext 'SecurityContextImpl@6ea1035d: Authentication: UsernamePasswordAuthenticationToken@6ea1035d: Principal: User@e85371e3: Username: TEST_USERNAME; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ROUTING_STATUS_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_ROUTING_STATUS_ADMIN' stored to HttpSession: 'MockHttpSession@294ab038
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 1 of 13 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 2 of 13 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
HttpSessionSecurityContextRepository:207 - Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: 'SecurityContextImpl@6ea1035d: Authentication: UsernamePasswordAuthenticationToken@6ea1035d: Principal: User@e85371e3: Username: TEST_USERNAME; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ROUTING_STATUS_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_ROUTING_STATUS_ADMIN'
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 3 of 13 in additional filter chain; firing Filter: 'HeaderWriterFilter'
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 4 of 13 in additional filter chain; firing Filter: 'CorsFilter'
FilterChainProxy$VirtualFilterChain:328 - /routing-status/ at position 5 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
CsrfFilter:110 - Invalid CSRF token found for http://localhost/routing-status/
HstsHeaderWriter:129 - Not injecting HSTS header since it did not match the requestMatcher HstsHeaderWriter$SecureRequestMatcher@247415be
SecurityContextPersistenceFilter:119 - SecurityContextHolder now cleared, as request processing completed
403 Forbidden
所以 csrf 似乎是个问题?但是,当应用程序为 运行 时,csrf 工作正常,我不确定它为什么要检查 http://localhost/routing-status
。当 运行 在本地时,在 localhost
和 /routing-status
之间有一个端口号和一个上下文路径。
我得到 403 的原因是因为测试中没有 CSRF 令牌。我对 CSRF 不是很熟悉,但是根据我目前的发现,它不会影响 GET 请求,因为 CSRF 只影响像 POST 和 PUT 这样的写请求。我仍在努力将 CSRF 步骤添加到 RestAssured 测试中,但至少我现在知道了该行为的原因。我打算按照 Rest Assured 的 github 页面上的说明完全解决问题:https://github.com/rest-assured/rest-assured/wiki/usage#csrf
这个问题的完美解决方案如下:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class TestClass {
@Autowired
private WebApplicationContext webApplicationContext;
@BeforeEach
public void setUp() {
RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
RestAssuredMockMvc.postProcessors(csrf().asHeader());
}
}