代码之家  ›  专栏  ›  技术社区  ›  matt b

弹簧安全单元测试

  •  114
  • matt b  · 技术社区  · 16 年前

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

    我们的安全要求是非常基本的;用户只需要提供用户名和密码就可以访问网站的某些部分(例如获取有关其帐户的信息);网站上有一些页面(常见问题解答、支持等),匿名用户应该可以访问这些页面。

    在我创建的原型中,我在会话中为经过身份验证的用户存储了一个“logincredentials”对象(它只包含用户名和密码);例如,一些控制器检查该对象是否在会话中以获取对登录用户名的引用。我正在寻找用Spring安全代替这种自行开发的逻辑,这将有一个很好的好处,可以删除任何类型的“我们如何跟踪登录用户?”以及“我们如何认证用户?”来自我的控制器/业务代码。

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

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

    …在某种程度上,这个物体是一个(全局的)单体,看起来非常不自然。

    我的问题是:如果这是在Spring Security中访问已验证用户信息的标准方法,那么在单元测试需要经过验证的用户时,将验证对象注入SecurityContext以便它可用于我的单元测试的公认方法是什么?

    我需要在每个测试用例的初始化方法中连接它吗?

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

    这似乎过于冗长。有更简单的方法吗?

    这个 SecurityContextHolder 物体本身看起来很不自然…

    11 回复  |  直到 8 年前
        1
  •  38
  •   cliff.meyers    16 年前

    问题在于,Spring安全性并不能使身份验证对象作为bean在容器中可用,因此无法轻松地将其注入或自动连接到框中。

    在开始使用Spring安全性之前,我们将在容器中创建一个会话范围的bean来存储主体,将其注入到“authenticationservice”(singleton)中,然后将此bean注入到需要了解当前主体的其他服务中。

    如果您正在实现自己的身份验证服务,那么基本上可以做相同的事情:创建一个具有“主体”属性的会话范围bean,将其注入到您的身份验证服务中,让auth服务将该属性设置为成功的auth,然后根据需要将auth服务提供给其他bean。

    使用SecurityContextHolder我不会感到太糟糕。不过。我知道它是一个静态的/单例的,Spring不鼓励使用这些东西,但是它们的实现会根据环境的不同而采取适当的行为:servlet容器中的会话范围、JUnit测试中的线程范围等等。单例的真正限制因素是它提供了一个不灵活的实现。不同的环境。

        2
  •  120
  •   TheKojuEffect    9 年前

    只需按常规方式进行,然后使用 SecurityContextHolder.setContext() 例如,在测试类中:

    控制器:

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

    测试:

    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);
    
        3
  •  27
  •   community wiki 3 revs, 3 users 97% Pavel    10 年前

    您应该关注的是非常正确的——静态方法调用对于单元测试尤其有问题,因为您不能轻易地模拟您的依赖性。我要向您展示的是如何让SpringIOC容器为您完成这些脏工作,从而为您留下整洁、可测试的代码。SecurityContextHolder是一个框架类,虽然您的低级安全代码可以绑定到它,但您可能希望向UI组件(即控制器)公开更整洁的接口。

    cliff.meyers提到了一种解决方法——创建自己的“主体”类型,并向消费者注入一个实例。春天& lt; aop:scoped-proxy />2.x中引入的标签与请求范围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代理标记的魔力,每次新的HTTP请求出现时都将调用静态方法getUserDetails,并且对currentUser属性的任何引用都将得到正确的解析。现在单元测试变得微不足道:

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

    希望这有帮助!

        4
  •  18
  •   matsev    8 年前

    在不回答如何创建和注入身份验证对象的问题的情况下,SpringSecurity4.0在测试方面提供了一些受欢迎的替代方案。这个 @WithMockUser 注释使开发人员能够以简洁的方式指定模拟用户(具有可选的权限、用户名、密码和角色):

    @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();
        ...
    }
    

    有关更多详细信息,请参见 @WithMockUser 以及 @WithUserDetails Spring安全参考文档中的章节(从中复制上述示例)

        5
  •  8
  •   user404345    12 年前

    我个人只会使用powermock和mock i to或easymock来模拟单元/集成测试中的静态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);
            ...
        }
    }
    

    诚然,这里有相当多的样板代码,例如模拟身份验证对象、模拟SecurityContext以返回身份验证,最后模拟SecurityContextHolder以获取SecurityContext,但是它非常灵活,允许您对场景(如空身份验证对象等)进行单元测试,而不必更改y。我们的(非测试)代码

        6
  •  5
  •   Michael Bushe    15 年前

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

    是的,静态通常是坏的-一般来说,但是在这种情况下,静态是您想要的。由于安全上下文将主体与当前运行的线程关联,因此最安全的代码将尽可能直接从线程访问静态。隐藏注入的包装类后面的访问为攻击者提供了更多的攻击点。他们不需要访问代码(如果jar被签名,他们很难更改代码),他们只需要一种方法来覆盖配置,可以在运行时完成,也可以将一些XML放到类路径上。即使使用注释注入,也可以用外部XML覆盖。这种XML可以向正在运行的系统注入一个恶意主体。

        7
  •  3
  •   Community T.Woody    7 年前

    我自己也问过同样的问题 here 刚刚发布了一个我最近找到的答案。简短的回答是:注入 SecurityContext 并参考 SecurityContextHolder 只有在您的Spring配置中才能获得 安全上下文

        8
  •  2
  •   digitalsanctum    16 年前

    我将看一下Spring的抽象测试类和模拟对象。 here . 它们提供了一种强大的自动连接Spring管理的对象的方法,使单元和集成测试更加容易。

        9
  •  2
  •   yankee    9 年前

    一般

    同时(从3.2版开始,在2013年,由于 SEC-2298 )可以使用注释将身份验证注入到MVC方法中 @AuthenticationPrincipal :

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

    测验

    在单元测试中,显然可以直接调用这个方法。在集成测试中使用 org.springframework.test.web.servlet.MockMvc 你可以使用 org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() 要像这样注入用户:

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

    但是,这将直接填充SecurityContext。如果要确保用户是从测试中的会话加载的,可以使用此选项:

    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;
            }
        };
    }
    
        10
  •  1
  •   Pavel Horal    11 年前

    身份验证是服务器环境中线程的属性,与操作系统中进程的属性相同。拥有一个用于访问身份验证信息的bean实例将不方便配置和连接开销,而且没有任何好处。

    关于测试认证,有几种方法可以让您的生活更轻松。我最喜欢做一个自定义注释 @Authenticated 测试执行侦听器,它管理它。检查 DirtiesContextTestExecutionListener 为了灵感。

        11
  •  0
  •   borjab    10 年前

    经过相当多的工作,我能够重现所期望的行为。我已经通过mockmvc模拟了登录。对于大多数单元测试来说,它太重了,但对集成测试很有帮助。

    当然,我愿意在SpringSecurity4.0中看到那些新特性,它们将使我们的测试更加容易。

    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 
    }