春季testing和安全:如何模拟身份validation?

我想弄清楚如何对我的控制器的URL进行适当的保护。 以防万一有人改变事情,并意外删除安全设置。

我的控制器方法如下所示:

@RequestMapping("/api/v1/resource/test") @Secured("ROLE_USER") public @ResonseBody String test() { return "test"; } 

我设置了一个WebTestEnvironment,如下所示:

 import javax.annotation.Resource; import javax.naming.NamingException; import javax.sql.DataSource; import org.junit.Before; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.FilterChainProxy; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/security.xml", "file:src/main/webapp/WEB-INF/spring/applicationContext.xml", "file:src/main/webapp/WEB-INF/spring/servlet-context.xml" }) public class WebappTestEnvironment2 { @Resource private FilterChainProxy springSecurityFilterChain; @Autowired @Qualifier("databaseUserService") protected UserDetailsService userDetailsService; @Autowired private WebApplicationContext wac; @Autowired protected DataSource dataSource; protected MockMvc mockMvc; protected final Logger logger = LoggerFactory.getLogger(this.getClass()); protected UsernamePasswordAuthenticationToken getPrincipal(String username) { UserDetails user = this.userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( user, user.getPassword(), user.getAuthorities()); return authentication; } @Before public void setupMockMvc() throws NamingException { // setup mock MVC this.mockMvc = MockMvcBuilders .webAppContextSetup(this.wac) .addFilters(this.springSecurityFilterChain) .build(); } } 

在我的实际testing中,我试图做这样的事情:

 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.Test; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import eu.ubicon.webapp.test.WebappTestEnvironment; public class CopyOfClaimTest extends WebappTestEnvironment { @Test public void signedIn() throws Exception { UsernamePasswordAuthenticationToken principal = this.getPrincipal("test1"); SecurityContextHolder.getContext().setAuthentication(principal); super.mockMvc .perform( get("/api/v1/resource/test") // .principal(principal) .session(session)) .andExpect(status().isOk()); } } 

我在这里select了这个:

  • http://java.dzone.com/articles/spring-test-mvc-junit-testing这里:
  • http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller或在这里:
  • 如何JUnittesting一个@PreAuthorize注解和由Spring MVC Controller指定的spring EL?

然而,如果仔细观察,只有在不向URL发送实际的请求时,只有在function级别上testing服务时才有所帮助。 在我的情况下,“拒绝访问”exception被抛出:

 org.springframework.security.access.AccessDeniedException: Access is denied at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE] ... 

以下两条日志消息值得注意,基本上说没有用户通过authentication,说明Principal设置不工作,或者被覆盖。

 14:20:34.454 [main] DEBUG ossaiaMethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER] 14:20:34.454 [main] DEBUG ossaiaMethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS 

事实certificate, SecurityContextPersistenceFilter是Spring Securityfilter链的一部分,它总是重置我的SecurityContext ,我设置了SecurityContextHolder.getContext().setAuthentication(principal) (或通过使用.principal(principal)方法)。 这个filter将SecurityContext中的SecurityContextHolder设置为SecurityContext中的SecurityContextHolder覆盖之前设置的SecurityContextRepository 。 版本库默认是HttpSessionSecurityContextRepositoryHttpSessionSecurityContextRepository检查给定的HttpRequest并尝试访问相应的HttpSession 。 如果存在,它将尝试从HttpSession读取SecurityContext 。 如果失败,存储库将生成一个空的SecurityContext

因此,我的解决scheme是传递一个HttpSession和包含SecurityContext的请求:

 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.Test; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import eu.ubicon.webapp.test.WebappTestEnvironment; public class Test extends WebappTestEnvironment { public static class MockSecurityContext implements SecurityContext { private static final long serialVersionUID = -1386535243513362694L; private Authentication authentication; public MockSecurityContext(Authentication authentication) { this.authentication = authentication; } @Override public Authentication getAuthentication() { return this.authentication; } @Override public void setAuthentication(Authentication authentication) { this.authentication = authentication; } } @Test public void signedIn() throws Exception { UsernamePasswordAuthenticationToken principal = this.getPrincipal("test1"); MockHttpSession session = new MockHttpSession(); session.setAttribute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, new MockSecurityContext(principal)); super.mockMvc .perform( get("/api/v1/resource/test") .session(session)) .andExpect(status().isOk()); } } 

在pom.xml中添加:

  <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <version>4.0.0.RC2</version> </dependency> 

并为授权请求使用org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors 。 请参阅https://github.com/rwinch/spring-security-test-blog(https://jira.spring.io/browse/SEC-2592 )上的示例用法。

更新:

4.0.0.RC2适用于spring-security 3.x. 对于spring-security 4,spring-security-test成为spring-security的一部分( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test ,版本相同)。

设置已更改: http : //docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

 public void setup() { mvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()) .build(); } 

基本身份validation示例: http : //docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication 。

寻找答案我找不到任何容易和灵活的同时,然后我发现了春季安全参考 ,我意识到有接近完美的解决scheme。 AOP解决scheme通常是testing最好的解决scheme,Spring在这个工件中提供了@WithMockUser@WithUserDetails@WithSecurityContext

 <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <version>4.2.2.RELEASE</version> <scope>test</scope> </dependency> 

在大多数情况下, @WithUserDetails收集我需要的灵活性和能力。

@WithUserDetails如何工作?

基本上,你只需要创build一个自定义UserDetailsService与所有可能的用户configuration文件,你想testing。 例如

 @TestConfiguration public class SpringSecurityWebAuxTestConfig { @Bean @Primary public UserDetailsService userDetailsService() { User basicUser = new UserImpl("Basic User", "user@company.com", "password"); UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList( new SimpleGrantedAuthority("ROLE_USER"), new SimpleGrantedAuthority("PERM_FOO_READ") )); User managerUser = new UserImpl("Manager User", "manager@company.com", "password"); UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList( new SimpleGrantedAuthority("ROLE_MANAGER"), new SimpleGrantedAuthority("PERM_FOO_READ"), new SimpleGrantedAuthority("PERM_FOO_WRITE"), new SimpleGrantedAuthority("PERM_FOO_MANAGE") )); return new InMemoryUserDetailsManager(Arrays.asList( basicActiveUser, managerActiveUser )); } } 

现在我们已经准备好了我们的用户,所以想象一下我们想要testing这个控制器function的访问控制:

 @RestController @RequestMapping("/foo") public class FooController { @Secured("ROLE_MANAGER") @GetMapping("/salute") public String saluteYourManager(@AuthenticationPrincipal User activeUser) { return String.format("Hi %s. Foo salutes you!", activeUser.getUsername()); } } 

在这里,我们有一个获得映射函数的路由/富/致敬 ,我们正在testing基于angular色的安全与@Secured注释,虽然你也可以testing@PreAuthorize@PostAuthorize 。 让我们创build两个testing,一个是检查一个有效的用户是否可以看到这个敬礼回应,另一个是检查是否真的被禁止。

 @RunWith(SpringRunner.class) @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SpringSecurityWebAuxTestConfig.class ) @AutoConfigureMockMvc public class WebApplicationSecurityTest { @Autowired private MockMvc mockMvc; @Test @WithUserDetails("manager@company.com") public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute") .accept(MediaType.ALL)) .andExpect(status().isOk()) .andExpect(content().string(containsString("manager@company.com"))); } @Test @WithUserDetails("user@company.com") public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute") .accept(MediaType.ALL)) .andExpect(status().isForbidden()); } } 

如您所见,我们导入了SpringSecurityWebAuxTestConfig来为我们的用户提供testing。 每个人使用相应的testing用例只需使用一个简单的注释,减less代码和复杂性。

更好地使用@WithMockUser更简单的基于angular色的安全性

正如您所看到的, @WithUserDetails具有您在大多数应用程序中所需的全部灵活性。 它允许您使用任何GrantedAuthority的自定义用户,如angular色或权限。 但是如果你只是使用angular色,testing可以更容易,你可以避免构造一个自定义的UserDetailsService 。 在这种情况下,用@WithMockUser指定用户,密码和angular色的简单组合。

 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented @WithSecurityContext( factory = WithMockUserSecurityContextFactory.class ) public @interface WithMockUser { String value() default "user"; String username() default ""; String[] roles() default {"USER"}; String password() default "password"; } 

注释为非常基本的用户定义了默认值。 在我们的情况下,我们正在testing的路由只需要经过身份validation的用户是经理,我们可以退出使用SpringSecurityWebAuxTestConfig并执行此操作。

 @Test @WithMockUser(roles = "MANAGER") public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute") .accept(MediaType.ALL)) .andExpect(status().isOk()) .andExpect(content().string(containsString("user"))); } 

注意,现在不是用户manager@company.com我们得到@WithMockUseruser提供的默认值。 然而这并不重要,因为我们真正关心的是他的angular色: ROLE_MANAGER

结论

正如您所看到的像@WithUserDetails@WithMockUser这样的注释,我们可以在不同的身份validation用户场景之间进行切换,而不需要为了进行简单的testing而构build与我们架构疏远的类。 它还build议你看看@WithSecurityContext如何工作,以获得更大的灵活性。

以下是一些使用Base64基本authenticationtestingSpring MockMvc安全configuration的人的例子。

 String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes())); this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); 

Maven依赖

  <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.3</version> </dependency> 

避免在testing中使用SecurityContextHolder的选项:

  • 选项1 :使用mocks – 我的意思是模拟SecurityContextHolder使用一些模拟库 – 例如EasyMock
  • 选项2 :在某些服务的代码中打包调用SecurityContextHolder.get... – 例如在SecurityServiceImpl中实现SecurityService接口的getCurrentPrincipal方法,然后在您的testing中,您可以简单地创build此接口的模拟实现,该接口返回期望的主体而不访问到SecurityContextHolder

简短的回答:

 @Autowired private WebApplicationContext webApplicationContext; @Autowired private Filter springSecurityFilterChain; @Before public void setUp() throws Exception { final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path"); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext) .defaultRequest(defaultRequestBuilder) .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest())) .apply(springSecurity(springSecurityFilterChain)) .build(); } private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder, final MockHttpServletRequest request) { requestBuilder.session((MockHttpSession) request.getSession()); return request; } 

从spring安全testing中执行formLogin之后,您的每个请求都将自动作为login用户调用。

很长的回答:

检查这个解决scheme(答案是春季4): 如何用spring 3.2login一个新的mvctesting用户