【发布时间】:2019-12-06 11:11:37
【问题描述】:
我有一个带有 JWT 身份验证的小型 Spring Boot 2.1.6 webapp。调用流程如下:
- 用户输入用户名和密码并向 /authenticate 发送 POST 请求
- 过滤器正在监视此 URL (setFilterProcessesUrl),当请求到来时,它会对密码进行哈希处理,并根据存储在 DB 中的哈希值进行检查
- 如果匹配,并且用户未被锁定,它会创建一个包含用户名和授予角色的 JWT,并在响应中返回它
- 用户必须在所有进一步的请求中包含此 JWT
此外,CSRF 在 WebSecurityConfigurerAdapter 中被禁用。
解决方案本身运行良好,但我还必须创建单元测试。我最终得到了以下测试用例:
@RunWith(SpringRunner.class)
@WebMvcTest
@ContextConfiguration(classes = { ConfigReaderMock.class })
public class ControllerSecurityTest {
private static final String VALID_USERNAME = "username";
private static final String VALID_PASSWORD = "password";
@Autowired
private MockMvc mockMvc;
private String createAuthenticationBody(String username, String passwordHash) {
return "username=" + URLEncoder.encode(username, StandardCharsets.UTF_8) + "&password="
+ URLEncoder.encode(passwordHash, StandardCharsets.UTF_8);
}
@Test
public void testValidLogin() throws Exception {
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders.post("/authenticate")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content(createAuthenticationBody(VALID_USERNAME, VALID_PASSWORD)).accept(MediaType.ALL))
.andExpect(status().isOk()).andReturn();
String authHeader = result.getResponse().getHeader(SecurityConstants.TOKEN_HEADER);
mockMvc.perform(MockMvcRequestBuilders.get("/main?" + SecurityConstants.TOKEN_QUERY_PARAM + "="
+ URLEncoder.encode(authHeader, StandardCharsets.UTF_8))).andExpect(status().isOk());
}
}
我期望的是,服务器接受提供的用户名和密码,并返回JWT,我可以在后续请求中使用它来访问下一页(在前端实现相同)。相反,我从身份验证过滤器中获得 HTTP 403:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /authenticate
Parameters = {username=[username], password=[password]}
Headers = [Content-Type:"application/x-www-form-urlencoded", Accept:"*/*"]
Body = <no character encoding set>
Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken@4ac0fdc7}
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 = 403
Error message = Forbidden
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:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
我注意到它出于某种原因在会话属性中发送了一个 CSRF 令牌。进一步检查日志,我可以看到以下消息:
2019-07-29 08:09:17,438 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration'
2019-07-29 08:09:17,443 DEBUG o.s.s.c.a.a.c.AuthenticationConfiguration$EnableGlobalAuthenticationAutowiredConfigurer [main] Eagerly initializing {org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration=org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration$$EnhancerBySpringCGLIB$$236da03c@4e68aede}
2019-07-29 08:09:17,444 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'inMemoryUserDetailsManager'
2019-07-29 08:09:17,445 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration'
2019-07-29 08:09:17,454 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties'
2019-07-29 08:09:17,457 DEBUG o.s.b.f.s.ConstructorResolver [main] Autowiring by type from bean name 'inMemoryUserDetailsManager' via factory method to bean named 'spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties'
2019-07-29 08:09:17,462 INFO o.s.b.a.s.s.UserDetailsServiceAutoConfiguration [main]
Using generated security password: 963b2bac-d953-4793-a8cd-b3f81586823e
...
2019-07-29 08:09:17,783 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository [main] No HttpSession currently exists
2019-07-29 08:09:17,784 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository [main] No SecurityContext was available from the HttpSession: null. A new one will be created.
2019-07-29 08:09:17,794 DEBUG o.s.s.w.c.CsrfFilter [main] Invalid CSRF token found for http://localhost/authenticate
2019-07-29 08:09:17,795 DEBUG o.s.s.w.h.w.HstsHeaderWriter [main] Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@1c15a6aa
2019-07-29 08:09:17,796 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper [main] SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2019-07-29 08:09:17,799 DEBUG o.s.s.w.c.SecurityContextPersistenceFilter [main] SecurityContextHolder now cleared, as request processing completed
所以看起来 Spring Security 正在创建它自己的安全配置,而不是使用我创建的类来扩展 WebSecurityConfigurerAdapter。问题是,为什么?以及如何强制它使用我的安全配置,因为我依赖它来登录数据库?
更新:添加了 WebSecurityConfigurerAdapter
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AICAuthenticationService authenticationService;
@Autowired
private AICUserDetailsService aicUserDetailsService;
@Autowired
private AICLogoutSuccessHandler aicLogoutSuccessHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.authorizeRequests()
.antMatchers("/resources/**", "/login", "/").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(aicLogoutSuccessHandler)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "error");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(aicUserDetailsService);
}
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return authenticationService;
}
@Bean
public AuthenticationManager custromAuthenticationManager() throws Exception {
return authenticationManager();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(aicUserDetailsService);
}
【问题讨论】:
-
你能加入你的
WebSecurityConfigurerAdapter吗?我可以想象/authenticate也受到您配置的某些规则的保护。 -
@MarcusHeld 添加了它。虽然正如我所提到的,当我将它作为“mvn spring-boot:run”运行时,同样可以正常工作,但只有在“mvn test”命令的情况下才会发生错误。
-
Spring Security 中已经有 JWT 过滤器了,为什么还要构建自己的过滤器?
-
@ThomasAndolf 你能提供一个例子吗?我浏览了许多在线教程,到处都创建了自定义过滤器。基本上我有两个过滤器:一个“AuthenticationFilter”,它从“/authenticate”的请求中获取用户名和密码,并尝试从数据库数据中进行身份验证,如果成功,它会创建 JWT,并返回它。另一个是“AuthorizationFilter”,它验证 JWT 是否存在于请求中(在标头中或在查询参数中)。
-
我的猜测是您已经在 baeldung.com 上查看了针对 spring security 4 的过时教程。我的建议是阅读 spring security 5 的官方文档。docs.spring.io/spring-security/site/docs/current/reference/…
标签: java spring-boot spring-security jwt