Unit testing with Spring Security

后端 未结 11 2172
暖寄归人
暖寄归人 2020-11-29 15:19

My company has been evaluating Spring MVC to determine if we should use it in one of our next projects. So far I love what I\'ve seen, and right now I\'m taking a look at th

相关标签:
11条回答
  • 2020-11-29 15:45

    You are quite right to be concerned - static method calls are particularly problematic for unit testing as you cannot easily mock your dependencies. What I am going to show you is how to let the Spring IoC container do the dirty work for you, leaving you with neat, testable code. SecurityContextHolder is a framework class and while it may be ok for your low-level security code to be tied to it, you probably want to expose a neater interface to your UI components (i.e. controllers).

    cliff.meyers mentioned one way around it - create your own "principal" type and inject an instance into consumers. The Spring <aop:scoped-proxy/> tag introduced in 2.x combined with a request scope bean definition, and the factory-method support may be the ticket to the most readable code.

    It could work like following:

    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
    }
    

    Nothing complicated so far, right? In fact you probably had to do most of this already. Next, in your bean context define a request-scoped bean to hold the principal:

    <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>
    

    Thanks to the magic of the aop:scoped-proxy tag, the static method getUserDetails will be called every time a new HTTP request comes in and any references to the currentUser property will be resolved correctly. Now unit testing becomes trivial:

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

    Hope this helps!

    0 讨论(0)
  • 2020-11-29 15:45

    Personally I would just use Powermock along with Mockito or Easymock to mock the static SecurityContextHolder.getSecurityContext() in your unit/integration test e.g.

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

    Admittedly there is quite a bit of boiler plate code here i.e. mock an Authentication object, mock a SecurityContext to return the Authentication and finally mock the SecurityContextHolder to get the SecurityContext, however its very flexible and allows you to unit test for scenarios like null Authentication objects etc. without having to change your (non test) code

    0 讨论(0)
  • 2020-11-29 15:46

    Just do it the usual way and then insert it using SecurityContextHolder.setContext() in your test class, for example:

    Controller:

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

    Test:

    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);
    
    0 讨论(0)
  • 2020-11-29 15:51

    The problem is that Spring Security does not make the Authentication object available as a bean in the container, so there is no way to easily inject or autowire it out of the box.

    Before we started to use Spring Security, we would create a session-scoped bean in the container to store the Principal, inject this into an "AuthenticationService" (singleton) and then inject this bean into other services that needed knowledge of the current Principal.

    If you are implementing your own authentication service, you could basically do the same thing: create a session-scoped bean with a "principal" property, inject this into your authentication service, have the auth service set the property on successful auth, and then make the auth service available to other beans as you need it.

    I wouldn't feel too bad about using SecurityContextHolder. though. I know that it's a static / Singleton and that Spring discourages using such things but their implementation takes care to behave appropriately depending on the environment: session-scoped in a Servlet container, thread-scoped in a JUnit test, etc. The real limiting factor of a Singleton is when it provides an implementation that is inflexible to different environments.

    0 讨论(0)
  • 2020-11-29 15:52

    After quite a lot of work I was able to reproduce the desired behavior. I had emulated the login through MockMvc. It is too heavy for most unit tests but helpful for integration tests.

    Of course I am willing to see those new features in Spring Security 4.0 that will make our testing easier.

    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 
    }
    
    0 讨论(0)
  • 2020-11-29 15:54

    Without answering the question about how to create and inject Authentication objects, Spring Security 4.0 provides some welcome alternatives when it comes to testing. The @WithMockUser annotation enables the developer to specify a mock user (with optional authorities, username, password and roles) in a neat way:

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

    There is also the option to use @WithUserDetails to emulate a UserDetails returned from the UserDetailsService, e.g.

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

    More details can be found in the @WithMockUser and the @WithUserDetails chapters in the Spring Security reference docs (from which the above examples were copied)

    0 讨论(0)
提交回复
热议问题