Spring测试和安全:如何模拟身份验证?
Spring测试和安全:如何模拟身份验证?
我试图找出如何单元测试我的控制器的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(); } }
在我的实际测试中,我尝试像这样执行:
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()); } }
我在这里学到了这个:
- http://java.dzone.com/articles/spring-test-mvc-junit-testing 这里:
- http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller 或者这里:
- 如何JUnit测试@PreAuthorize注释及其由spring MVC Controller指定的Spring EL?
然而,如果仔细看的话,这只有在不向URL发送实际请求,而只在测试函数级别时才有帮助。在我的情况下,抛出了“访问被拒绝”的异常:
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] ...
下面的两条日志信息是值得注意的,基本上表明没有用户被认证,这意味着设置了Principal
失败了,或者被覆盖了。
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - 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 o.s.s.a.i.a.MethodSecurityInterceptor - 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
从Spring 4.0+开始,最好的解决方案是使用@WithMockUser注释测试方法
@Test @WithMockUser(username = "user1", password = "pwd", roles = "USER") public void mytest1() throws Exception { mockMvc.perform(get("/someApi")) .andExpect(status().isOk()); }
记得将以下依赖项添加到您的项目中
'org.springframework.security:spring-security-test:4.2.3.RELEASE'
在寻找易用和灵活性之间的答案时,我找到了Spring Security参考,我意识到那里有近乎完美的解决方案。在测试中,AOP解决方案通常是最好的解决方案,Spring提供了@WithMockUser
,@WithUserDetails
和@WithSecurityContext
,以实现这个工件:\n
org.springframework.security spring-security-test 4.2.2.RELEASE test
\n在大多数情况下,@WithUserDetails
提供了我所需的灵活性和功能。\n
如何使用@WithUserDetails?
\n基本上,你只需要创建一个自定义的UserDetailsService
,其中包含你想要测试的所有可能的用户配置文件。例如:\n
@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 )); } }
\n现在,我们的用户已经准备好了,所以假设我们想要测试对这个控制器函数的访问控制:\n
@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()); } }
\n在这里,我们有一个映射到路由/foo/salute的get函数,并且我们正在使用@Secured
注释进行角色基础安全测试,尽管你也可以进行@PreAuthorize
和@PostAuthorize
测试。\n让我们创建两个测试,一个用于检查有效用户是否可以看到这个敬礼响应,另一个用于检查它是否真的被禁止。\n
@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()); } }
\n正如你所看到的,我们导入了 SpringSecurityWebAuxTestConfig
,为测试提供我们的用户。每个测试案例使用其对应的简单注释即可。这样可以减少代码和复杂性。\n
更好的使用@WithMockUser进行简单的基于角色的安全性
\n正如你所看到的,@WithUserDetails
具有大多数应用程序所需的所有灵活性。它允许你使用任何GrantedAuthority,如角色或权限的自定义用户。但是如果你只使用角色进行工作,那么测试就会变得更加简单,可以避免构建自定义 UserDetailsService
。在这种情况下,使用@WithMockUser指定简单的用户、密码和角色组合即可。\n
@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"; }
\n该注释为一个非常基本的用户定义默认值。由于我们正在测试的路由只要求已认证的用户是经理,因此我们可以放弃使用 SpringSecurityWebAuxTestConfig
,并且可以这样做。\n
@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"))); }
\n请注意,现在我们不再使用用户 manager@company.com ,而是使用@WithMockUser
提供的默认用户: user ;然而,这并不重要,因为我们真正关心的是他的角色: ROLE_MANAGER
。\n
结论
\n如你所看到的,使用注释@WithUserDetails
和@WithMockUser
,我们可以在不构建与我们的架构疏远的类的情况下,在不同的认证用户场景之间切换,以进行简单的测试。还建议您查看@WithSecurityContext,以获得更灵活的功能。