我试图弄清楚如何对我的控制器的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()); } }
JUnit如何测试@PreAuthorize批注及其由spring MVC控制器指定的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
事实证明,作为SecurityContextPersistenceFilterSpring Security过滤器链的一部分的,始终会重置my SecurityContext,而我会设置调用SecurityContextHolder.getContext().setAuthentication(principal)(或使用.principal(principal)方法)。该过滤器设置SecurityContext在SecurityContextHolder同一个SecurityContext从SecurityContextRepository覆盖一个我先前设置。HttpSessionSecurityContextRepository默认情况下,该存储库为。被HttpSessionSecurityContextRepository检查的给定HttpRequest并尝试访问相应的HttpSession。如果它存在,它会尝试读取SecurityContext距离HttpSession。如果失败,则存储库将生成一个empty SecurityContext。
SecurityContextPersistenceFilterSpring Security
my SecurityContext
SecurityContextHolder.getContext().setAuthentication(principal)
.principal(principal)
SecurityContext
SecurityContextHolder
SecurityContextRepository
HttpSessionSecurityContextRepository
HttpSession
empty SecurityContext
因此,我的解决方案是将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 {
我找到了Spring Security Reference,并且意识到有接近完美的解决方案。AOP的解决方案往往是最伟大的人进行测试,并且Spring提供了它@WithMockUser,@WithUserDetails并@WithSecurityContext在此神器:
@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如何工作?
基本上,你只需要UserDetailsService使用要测试的所有可能的用户配置文件创建一个自定义。例如
UserDetailsService
@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 )); } }
现在我们已经为用户准备好了,所以想象我们要测试对该控制器功能的访问控制:
@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()); } }
在这里,我们有一个被映射功能的路线/富/敬礼,我们正在测试与基于角色的安全性@Secured注解,但你可以测试@PreAuthorize和@PostAuthorize也。让我们创建两个测试,一个用于检查有效用户是否可以看到此敬礼响应,另一个用于检查它是否被实际禁止。
@PreAuthorize
@PostAuthorize
@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为了为我们的用户提供测试。每个人仅通过使用简单的注释就可以在其相应的测试用例上使用,从而减少了代码和复杂性。
SpringSecurityWebAuxTestConfig
更好地使用@WithMockUser获得更简单的基于角色的安全性 如你所见,它@WithUserDetails具有大多数应用程序所需的所有灵活性。它允许你使用具有任何GrantedAuthority(例如角色或权限)的自定义用户。但是,如果你只是使用角色,则测试甚至会更加容易,并且可以避免构造一个custom UserDetailsService。在这种情况下,请使用@WithMockUser指定用户,密码和角色的简单组合。
GrantedAuthority
custom UserDetailsService
@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"; }
批注为非常基本的用户定义默认值。就像在我们的案例中一样,我们正在测试的路线仅要求经过身份验证的用户成为管理员,我们可以退出使用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我们正在提供的默认@WithMockUser:用户 ; 但这并不重要,因为我们真正关心的是他的角色:ROLE_MANAGER。
结论 如你所见,使用注释这样的注释@WithUserDetails,@WithMockUser我们可以在不同的经过身份验证的用户场景之间进行切换,而无需构建与我们的体系结构无关的类,仅用于进行简单测试。它还建议你查看@WithSecurityContext如何工作以提供更大的灵活性。