java - 为什么使用 MockMVC MockMvc 时 Spring Security CSRF 检查失败
问题描述
抱歉信息超载,我认为这里的信息比需要的多得多。
所以我有这个测试代码
package com.myapp.ui.controller.user;
import static com.myapp.ui.controller.user.PasswordResetController.PASSWORD_RESET_PATH;
import static com.myapp.ui.controller.user.PasswordResetController.ResetPasswordAuthenticatedDto;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.MediaType;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.myapp.db.liquibase.LiquibaseService;
import com.myapp.sample.ModelBuilderFactory;
@ComponentScan( "com.myapp.ui")
@SpringJUnitWebConfig( classes = { LiquibaseService.class, ControllerTestBaseConfig.class } )
public class PasswordResetControllerTest {
@Autowired
private ModelBuilderFactory mbf;
private final ObjectMapper mapper = new ObjectMapper();
private MockMvc mockMvc;
private String username;
@BeforeEach
void setup( WebApplicationContext wac) throws Exception {
username = mbf.siteUser().get().getUsername();
this.mockMvc = MockMvcBuilders.webAppContextSetup( wac )
.apply( SecurityMockMvcConfigurers.springSecurity() )
.defaultRequest( get( "/" ).with( user(username) ) )
.alwaysDo( print() )
.build();
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = "abcd")
void testPasswordResetInvalidate( String password ) throws Exception {
ResetPasswordAuthenticatedDto dto = new ResetPasswordAuthenticatedDto( username, password );
RequestBuilder builder = MockMvcRequestBuilders.patch( PASSWORD_RESET_PATH )
.contentType( MediaType.APPLICATION_JSON )
.content( mapper.writer().writeValueAsString( dto ) );
mockMvc.perform( builder )
.andExpect( MockMvcResultMatchers.status().isNoContent() );
}
}
这是相关的调试输出
[INFO ] PATCH /ui/authn/user/password-reset for user null (session 1, csrf f47a7f39-c310-4e5d-a4be-cc9bd120229f) with session cookie (null) will be validated against CSRF [main] com.myapp.config.MyCsrfRequestMatcher.matches(MyCsrfRequestMatcher.java:76)
[DEBUG] Invalid CSRF token found for http://localhost/ui/authn/user/password-reset [main] org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:127)
[DEBUG] Starting new session (if required) and redirecting to '/login?error=timeout' [main] org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy.onInvalidSessionDetected(SimpleRedirectInvalidSessionStrategy.java:49)
[DEBUG] Redirecting to '/login?error=timeout' [main] org.springframework.security.web.DefaultRedirectStrategy.sendRedirect(DefaultRedirectStrategy.java:54)
[DEBUG] Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@4b367665 [main] org.springframework.security.web.header.writers.HstsHeaderWriter.writeHeaders(HstsHeaderWriter.java:169)
[DEBUG] SecurityContextHolder now cleared, as request processing completed [main] org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:119)
MockHttpServletRequest:
HTTP Method = PATCH
Request URI = /ui/authn/user/password-reset
Parameters = {}
Headers = [Content-Type:"application/json", Content-Length:"68"]
Body = <no character encoding set>
Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken@55b1156b, SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@12919b87: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@12919b87: Principal: org.springframework.security.core.userdetails.User@1e05232c: Username: lab_admin4oyc8tkytxu4zllguk-1qg; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 302
Error message = null
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"SAMEORIGIN", Location:"/login?error=timeout"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = /login?error=timeout
Cookies = []
java.lang.AssertionError: Status expected:<204> but was:<302>
Expected :204
Actual :302
这是我们的相关SecurityConfig
@Override
protected void configure( HttpSecurity http ) throws Exception {
ApplicationContext ctx = getApplicationContext();
http
.addFilterAfter( ctx.getBean( TimeoutFilter.class ), SecurityContextPersistenceFilter.class )
.addFilterAt( ctx.getBean( "myLogoutFilter", LogoutFilter.class ), LogoutFilter.class )
.addFilterBefore( ctx.getBean( IpAddressAuditFilter.class ), FilterSecurityInterceptor.class )
.addFilterAt( ctx.getBean( MyConcurrentSessionFilter.class ), ConcurrentSessionFilter.class )
.addFilterAfter( ctx.getBean( RequestLogFilter.class ), FilterSecurityInterceptor.class )
.headers().xssProtection().and().frameOptions().sameOrigin().and()
.authorizeRequests()
.antMatchers(
HttpMethod.GET,
"/styles/**.css",
"/fonts/**",
"/scripts/**.js",
"/VAADIN/**",
"/jsp/**",
"/*.css",
"/help/**",
"/public/error/**"
).permitAll()
.antMatchers( "/app/**", "/downloads/**", "/ui/authn/**" ).fullyAuthenticated()
.antMatchers(
"/login**",
"/register/**",
"/public/**",
"/sso_logout",
"/sso_auth_failure",
"/sso_concurrent_session",
"/sso_timeout"
).anonymous()
.anyRequest().denyAll()
.and()
.formLogin()
.loginPage( "/login" )
.loginProcessingUrl( SecurityConstants.loginPath() )
.defaultSuccessUrl( "/app", true )
.failureUrl( "/login?error=badCredentials" )
.usernameParameter( SecurityConstants.usernameField() )
.passwordParameter( SecurityConstants.passwordField())
.permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler( new MyAccessDeniedHandler() )
.authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint( "/login" ) )
.and()
.csrf().requireCsrfProtectionMatcher( csrfRequestMatcher )
.and()
.sessionManagement().sessionFixation().newSession().invalidSessionUrl( "/login?error=timeout" )
.maximumSessions( 1 ).sessionRegistry( sessionRegistry )
.expiredUrl( "/login?error=concurrentSession" );
}
测试正在重定向到,timeout
因为它认为会话无效。
我们使用 spring boot 父 BOM仅用于依赖管理,我们尚未转换为 spring boot (WIP)。包含信息,以便您了解我们正在使用的版本。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.8.RELEASE</version>
<relativePath />
</parent>
鉴于我正在使用弹簧安全性user()
并springSecurity()
设置 MockMvc,我不确定为什么 CSRF 不正确。我将我的路径添加到我们在检查 CSRF 时排除的路径中,这个问题就消失了。我需要做什么才能让 CSRF 检查通过?
解决方案
看起来您在测试中的任何地方都没有为您的请求生成 CSRF 令牌。日志确实显示您那里有一些 CSRF 令牌,但如果您没有在请求中提供此类令牌,CsrfFilter 将为您创建一个并与该令牌进行比较,我希望它会以您报告的方式失败。
SecurityMockMvcRequestPostProcessors.csrf()
您可以通过作为参数为mockMvc 请求创建一个 csrf 令牌mockMvc.perform(builder).with( ... )
。
为了检查您的请求中是否有 csrf 令牌,我将通过调试器运行它并在 CsrfFilter 的第 120-123 行周围放置一个断点。
推荐阅读
- java - Java 原生包要求
- javascript - 尝试在反应应用程序客户端上构建或启动纱线时出现大问题
- c - cygwin中的freeglut.a在哪里?
- reactjs - React Redux - 加载状态太慢 - 如何解决
- javascript - Discord.js Bot 赠品命令:.array() 不是函数
- c++ - 如何检查文件是否被 C++ 中的另一个进程使用?
- python - 如何在 Sympy 中简化包含复指数的表达式
- php - PHP:在文本日志文件中查询而不是数据库记录
- php - 如果一个主题在它的functions.php中加入了一个样式,你如何在子主题中取消注册以在更新主题时保留它?
- python - 如何删除 Jupyter 笔记本上的变量检查器?