How do I unit test spring security @PreAuthorize(hasRole)?

前端 未结 3 650
暖寄归人
暖寄归人 2020-12-24 12:15

What do I need in order to unit test the hasRole part of a PreAuthorize annotation on a controller method?

My test should succeed because the logged in user only has

相关标签:
3条回答
  • 2020-12-24 12:53

    MockMvcBuilders.standaloneSetup gets a MyController instantiated manually ( without Spring and therefore without AOP). Therefore the PreAuthorize is not intercepted and security check is skipped. You can therefore @Autowire your controller and pass it to MockMvcBuilders.standaloneSetup to mock any services passed to the controller (as it's sometimes needed) use @MockBean so that every instance of the service gets replaced with the Mock.

    0 讨论(0)
  • 2020-12-24 12:57

    Just to add to Rob's solution above, as of December 20, 2014, there is a bug in the SecurityRequestPostProcessors class on the master branch from Rob's answer above that prevents the assigned roles from being populated.

    A quick fix is to comment out the following line of code (currently line 181) in the roles(String... roles) method of the UserRequestPostProcessor inner static class of SecurityRequestPostProcessors:

    // List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(roles.length);.

    You need to comment out the local variable, NOT the member variable.

    Alternatively, you may insert this line just before returning from the method:

    this.authorities = authorities;

    P.S I would have added this as a comment had I had enough reputation.

    0 讨论(0)
  • 2020-12-24 13:12

    UPDATE

    Spring Security 4 provides comprehensive support for integrating with MockMvc. For example:

    import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration
    @WebAppConfiguration
    public class SecurityMockMvcTests {
    
        @Autowired
        private WebApplicationContext context;
    
        private MockMvc mvc;
    
        @Before
        public void setup() {
            mvc = MockMvcBuilders
                    .webAppContextSetup(context)
                    .apply(springSecurity())
                    .build();
        }
    
        @Test
        public void withUserRequestPostProcessor() {
            mvc
                .perform(get("/admin").with(user("admin").roles("USER","ADMIN")))
                ...
        }
    
        @WithMockUser(roles="ADMIN")
        @Test
        public void withMockUser() {
            mvc
                .perform(get("/admin"))
                ...
        }
    
     ...
    

    The Problem

    The problem is that setting the SecurityContextHolder does not work in this instance. The reason is that the SecurityContextPersistenceFilter will use the SecurityContextRepository to try and figure out the SecurityContext from the HttpServletRequest (by default it uses the HttpSession). The SecurityContext it finds (or doesn't find) will override the SecurityContext you have set on the SecurityContextHolder.

    The Solution

    To ensure the request is authenticated you need to associate your SecurityContext using the SecurityContextRepository that you are leveraging. The default is the HttpSessionSecurityContextRepository. An example method that will allow you to mock being logged in by a user is below:

    private SecurityContextRepository repository = 
          new HttpSessionSecurityContextRepository();
    
    private void login(SecurityContext securityContext, HttpServletRequest request) {
        HttpServletResponse response = new MockHttpServletResponse();
    
        HttpRequestResponseHolder requestResponseHolder = 
              new HttpRequestResponseHolder(request, response);
        repository.loadContext(requestResponseHolder);
    
        request = requestResponseHolder.getRequest();
        response = requestResponseHolder.getResponse();
    
        repository.saveContext(securityContext, request, response);
    }
    

    The details of how to use this might still a bit vague since you might not know how to access the HttpServletRequest in MockMvc, but keep reading as there is a better solution.

    Making it easier

    If you want to make this and other Security related interactions with MockMvc easier, you can refer to the gs-spring-security-3.2 sample application. Within the project you will find some utilities for working with Spring Security and MockMvc called SecurityRequestPostProcessors. To use them you can copy that previously mentioned class into your project. Using this utility will allow you to write something like this instead:

    RequestBuilder request = get("/110")
        .with(user(rob).roles("USER"));
    
    mvc
        .perform(request)
        .andExpect(status().isUnAuthorized());
    

    NOTE: There is no need to set the principal on the request as Spring Security establishes the Principal for you as long as a user is authenticated.

    You can find additional examples in SecurityTests. This project will also assist in other integrations between MockMvc and Spring Security (i.e. setting up the request with the CSRF token when performing a POST).

    Not included by default?

    You might ask why this is not included by default. The answer is that we simply did not have time for the 3.2 timeline. All the code in the sample will work fine, but we weren't confident enough on naming conventions and exactly how it integrated to release this. You can track SEC-2015 which is scheduled to come out with Spring Security 4.0.0.M1.

    Update

    Your MockMvc instance needs to also contain the springSecurityFilterChain. To do so, you can use the following:

    @Autowired
    private Filter springSecurityFilterChain;
    
    @Test
    public void testValidUserWithInvalidRoleFails() throws Exception {
        MockMvc mockMvc = standaloneSetup(myController)
            .addFilters(springSecurityFilterChain)
            .setViewResolvers(viewResolver())
            .build();
        ...
    

    For the @Autowired to work, you need to ensure to include your security configuration that makes the springSecurityFilterChain in your @ContextConfiguration. For your current setup, this means "classpath:/spring/abstract-security-test.xml" should contain your <http ..> portion of your security configuration (and all the dependent beans). Alternatively, you can include a second file(s) in the @ContextConfiguration that has your <http ..> portion of your security configuration (and all the dependent beans).

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