用Spring Security进行unit testing

我的公司一直在评估Spring MVC,以确定我们是否应该在下一个项目中使用它。 到目前为止,我喜欢我所看到的,现在我正在看看Spring Security模块,以确定它是我们可以/应该使用的东西。

我们的安全要求是相当基础的。 用户只需提供用户名和密码即可访问网站的某些部分(如获取有关其帐户的信息); 并且网站上有一些页面(常见问题,支持等),匿名用户应该被授予访问权限。

在我创build的原型中,我一直在Session中为已authentication的用户存储一个“LoginCredentials”对象(它只包含用户名和密码) 例如,一些控制器检查这个对象是否在会话中以获得对login用户名的引用。 我期待用Spring Security取代这个本土化的逻辑,而不是去除任何types的“我们如何跟踪login用户?”的好处。 和“我们如何authentication用户?” 从我的控制器/业务代码。

Spring Security似乎提供了一个(每线程)“上下文”对象,可以从应用程序的任何位置访问用户名/主体信息。

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 

…这似乎是非常像spring一样,因为这个对象是(全局)单身,在某种程度上。

我的问题是:如果这是在Spring Security中访问经过身份validation的用户的信息的标准方法,那么在SecurityContext中注入一个Authentication对象的方法是什么,以便在unit testing需要authentication用户?

我是否需要在每个testing用例的初始化方法中进行连线?

 protected void setUp() throws Exception { ... SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword())); ... } 

这似乎过于冗长。 有更容易的方法吗?

SecurityContextHolder对象本身看起来非常像Spring一样…

问题是,Spring Security不会使Authentication对象在容器中作为一个bean来使用,所以没有办法轻易地将它注入或自动装入。

在我们开始使用Spring Security之前,我们将在容器中创build一个会话范围的bean来存储Principal,将其注入到一个“AuthenticationService”(singleton)中,然后将这个bean注入到需要了解当前Principal的其他服务中。

如果您正在实现自己的身份validation服务,则基本上可以做同样的事情:使用“主体”属性创build会话范围的bean,将其注入到身份validation服务中,使auth服务将该属性设置为成功身份validation,然后根据需要使auth服务可用于其他bean。

我不会觉得使用SecurityContextHolder太糟糕了。 虽然。 我知道这是一个静态/单例,Spring不鼓励使用这些东西,但是它们的实现需要根据环境来适当地运行:在Servlet容器中进行会话作用域,在JUnittesting中进行线程作用域等。真正的限制因素一个单例就是当它提供一个对不同环境不灵活的实现时。

只要按照通常的方式进行操作,然后在testing类中使用SecurityContextHolder.setContext()插入它,例如:

控制器:

 Authentication a = SecurityContextHolder.getContext().getAuthentication(); 

testing:

 Authentication authentication = Mockito.mock(Authentication.class); // Mockito.whens() for your authorization object SecurityContext securityContext = Mockito.mock(SecurityContext.class); Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); 

静态方法调用对于unit testing来说尤其有问题,因为你不能轻易地模拟你的依赖关系。 我要告诉你的是如何让Spring IoC容器为你做肮脏的工作,给你留下整洁,可testing的代码。 SecurityContextHolder是一个框架类,尽pipe你的低级安全代码可能会被绑定到它,但是你可能想要为你的UI组件(即控制器)公开一个整洁的接口。

cliff.meyers提到了一个方法 – 创build自己的“主体”types,并向消费者注入一个实例。 2.x中引入的Spring < aop:scoped-proxy />标记与请求作用域bean定义相结合,工厂方法支持可能是最可读代码的票据。

它可以工作如下:

 public class MyUserDetails implements UserDetails { // this is your custom UserDetails implementation to serve as a principal // implement the Spring methods and add your own methods as appropriate } public class MyUserHolder { public static MyUserDetails getUserDetails() { Authentication a = SecurityContextHolder.getContext().getAuthentication(); if (a == null) { return null; } else { return (MyUserDetails) a.getPrincipal(); } } } public class MyUserAwareController { MyUserDetails currentUser; public void setCurrentUser(MyUserDetails currentUser) { this.currentUser = currentUser; } // controller code } 

没有什么复杂的,到目前为止? 事实上,你可能已经做了大部分。 接下来,在您的bean上下文中定义一个请求范围的bean来容纳主体:

 <bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request"> <aop:scoped-proxy/> </bean> <bean id="controller" class="MyUserAwareController"> <property name="currentUser" ref="userDetails"/> <!-- other props --> </bean> 

感谢aop:scoped-proxy标签的魔力,静态方法getUserDetails将在每次新的HTTP请求进入时被调用,并且任何对currentUser属性的引用都将被正确parsing。 现在unit testing变得微不足道了:

 protected void setUp() { // existing init code MyUserDetails user = new MyUserDetails(); // set up user as you wish controller.setCurrentUser(user); } 

希望这可以帮助!

在不回答关于如何创build和注入Authentication对象的问题时,Spring Security 4.0在testing时提供了一些值得欢迎的select。 @WithMockUser注释使开发人员能够以一种简洁的方式指定模拟用户(包括可选的权限,用户名,密码和angular色):

 @Test @WithMockUser(username = "admin", authorities = { "ADMIN", "USER" }) public void getMessageWithMockUserCustomAuthorities() { String message = messageService.getMessage(); ... } 

也可以使用@WithUserDetails来模拟从UserDetails返回的UserDetailsService ,例如

 @Test @WithUserDetails("customUsername") public void getMessageWithUserDetailsCustomUsername() { String message = messageService.getMessage(); ... } 

更多的细节可以在Spring安全参考文档的@WithMockUser和@WithUserDetails章节中find(上面的示例复制了这些章节)

就个人而言,我只是将Powermock与Mockito或Easymock一起用于模拟单元/集成testing中的静态SecurityContextHolder.getSecurityContext(),例如

 @RunWith(PowerMockRunner.class) @PrepareForTest(SecurityContextHolder.class) public class YourTestCase { @Mock SecurityContext mockSecurityContext; @Test public void testMethodThatCallsStaticMethod() { // Set mock behaviour/expectations on the mockSecurityContext when(mockSecurityContext.getAuthentication()).thenReturn(...) ... // Tell mockito to use Powermock to mock the SecurityContextHolder PowerMockito.mockStatic(SecurityContextHolder.class); // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext() Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext); ... } } 

诚然,这里有相当多的锅炉代码,即模拟一个authentication对象,模拟一个SecurityContext返回authentication,最后模拟SecurityContextHolder获得SecurityContext,但它非常灵活,并允许您unit testing的情况下,如空authentication对象等等,而不必改变你的(非testing)代码

在这种情况下使用静态是编写安全代码的最佳方法。

是的,静力学通常是不好的 – 一般来说,但在这种情况下,静态是你想要的。 由于安全上下文将Principal与当前正在运行的线程相关联,因此最安全的代码将尽可能直接从线程访问静态。 隐藏被注入的包装类后面的访问提供了攻击者更多的攻击点。 他们不需要访问代码(如果jar被签名,他们将很难改变),他们只需要一种方法来覆盖configuration,这可以在运行时完成,或者将一些XML放到classpath中。 即使使用注释注入也会被外部XML覆盖。 这样的XML可以将运行系统注入stream氓主体。

我在这里问了同样的问题,刚刚发布了我最近发现的答案。 简单的答案是:注入一个SecurityContext ,并且仅在您的Springconfiguration文件中引用SecurityContextHolder来获取SecurityContext

我会看看Spring的抽象testing类和这里讨论的模拟对象。 它们提供了一个强大的自动连接你的Springpipe理对象的方法,使得单元和集成testing更容易。

一般

与此同时(从3.2版本开始,在2013年,感谢SEC-2298 ),可以使用注解@AuthenticationPrincipal将身份validation注入到MVC方法中:

 @Controller class Controller { @RequestMapping("/somewhere") public void doStuff(@AuthenticationPrincipal UserDetails myUser) { } } 

testing

在你的unit testing中,你可以直接调用这个方法。 在使用org.springframework.test.web.servlet.MockMvc集成testing中,可以使用org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user()来注入用户,如下所示:

 mockMvc.perform(get("/somewhere").with(user(myUserDetails))); 

这将直接填充SecurityContext。 如果你想确保用户在你的testing中从一个会话加载,你可以使用这个:

 mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails))); /* ... */ private static RequestPostProcessor sessionUser(final UserDetails userDetails) { return new RequestPostProcessor() { @Override public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) { final SecurityContext securityContext = new SecurityContextImpl(); securityContext.setAuthentication( new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()) ); request.getSession().setAttribute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext ); return request; } }; } 

身份validation是服务器环境中线程的属性,方式与OS中进程的属性相同。 拥有一个用于访问authentication信息的bean实例将是不便的configuration和布线开销,没有任何好处。

关于testingauthentication,有几种方法可以让你的生活更轻松。 我最喜欢的是做一个自定义注释@Authenticated和testing执行监听器,pipe理它。 检查DirtiesContextTestExecutionListener的灵感。

经过相当多的工作,我能够重现所需的行为。 我通过MockMvc模拟了login。 这对于大多数unit testing来说太重了,但是对集成testing很有帮助。

当然,我愿意看到Spring Security 4.0中的这些新function,这些function将使我们的testing变得更加简单。

 package [myPackage] import static org.junit.Assert.*; import javax.inject.Inject; import javax.servlet.http.HttpSession; import org.junit.Before; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; 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; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @ContextConfiguration(locations={[my config file locations]}) @WebAppConfiguration @RunWith(SpringJUnit4ClassRunner.class) public static class getUserConfigurationTester{ private MockMvc mockMvc; @Autowired private FilterChainProxy springSecurityFilterChain; @Autowired private MockHttpServletRequest request; @Autowired private WebApplicationContext webappContext; @Before public void init() { mockMvc = MockMvcBuilders.webAppContextSetup(webappContext) .addFilters(springSecurityFilterChain) .build(); } @Test public void testTwoReads() throws Exception{ HttpSession session = mockMvc.perform(post("/j_spring_security_check") .param("j_username", "admin_001") .param("j_password", "secret007")) .andDo(print()) .andExpect(status().isMovedTemporarily()) .andExpect(redirectedUrl("/index")) .andReturn() .getRequest() .getSession(); request.setSession(session); SecurityContext securityContext = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); SecurityContextHolder.setContext(securityContext); // Your test goes here. User is logged with }