Spring Test & Security: How to mock authentication?

后端 未结 7 1490
野性不改
野性不改 2020-11-28 01:27

I was trying to figure out how to unit test if my the URLs of my controllers are properly secured. Just in case someone changes things around and accidentally removes securi

相关标签:
7条回答
  • 2020-11-28 01:41

    It turned out that the SecurityContextPersistenceFilter, which is part of the Spring Security filter chain, always resets my SecurityContext, which I set calling SecurityContextHolder.getContext().setAuthentication(principal) (or by using the .principal(principal) method). This filter sets the SecurityContext in the SecurityContextHolder with a SecurityContext from a SecurityContextRepository OVERWRITING the one I set earlier. The repository is a HttpSessionSecurityContextRepository by default. The HttpSessionSecurityContextRepository inspects the given HttpRequest and tries to access the corresponding HttpSession. If it exists, it will try to read the SecurityContext from the HttpSession. If this fails, the repository generates an empty SecurityContext.

    Thus, my solution is to pass a HttpSession along with the request, which holds the 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 {
    
            UsernamePasswordAuthenticationToken principal = 
                    this.getPrincipal("test1");
    
            MockHttpSession session = new MockHttpSession();
            session.setAttribute(
                    HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, 
                    new MockSecurityContext(principal));
    
    
            super.mockMvc
                .perform(
                        get("/api/v1/resource/test")
                        .session(session))
                .andExpect(status().isOk());
        }
    }
    
    0 讨论(0)
  • 2020-11-28 01:43

    Short answer:

    @Autowired
    private WebApplicationContext webApplicationContext;
    
    @Autowired
    private Filter springSecurityFilterChain;
    
    @Before
    public void setUp() throws Exception {
        final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
                .defaultRequest(defaultRequestBuilder)
                .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
                .apply(springSecurity(springSecurityFilterChain))
                .build();
    }
    
    private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
                                                                 final MockHttpServletRequest request) {
        requestBuilder.session((MockHttpSession) request.getSession());
        return request;
    }
    

    After perform formLogin from spring security test each of your requests will be automatically called as logged in user.

    Long answer:

    Check this solution (the answer is for spring 4): How to login a user with spring 3.2 new mvc testing

    0 讨论(0)
  • 2020-11-28 01:54

    Options to avoid using SecurityContextHolder in tests:

    • Option 1: use mocks - I mean mock SecurityContextHolder using some mock library - EasyMock for example
    • Option 2: wrap call SecurityContextHolder.get... in your code in some service - for example in SecurityServiceImpl with method getCurrentPrincipal that implements SecurityService interface and then in your tests you can simply create mock implementation of this interface that returns the desired principal without access to SecurityContextHolder.
    0 讨论(0)
  • 2020-11-28 02:03

    Seaching for answer I couldn't find any to be easy and flexible at the same time, then I found the Spring Security Reference and I realized there are near to perfect solutions. AOP solutions often are the greatest ones for testing, and Spring provides it with @WithMockUser, @WithUserDetails and @WithSecurityContext, in this artifact:

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.2.2.RELEASE</version>
        <scope>test</scope>
    </dependency>
    

    In most cases, @WithUserDetails gathers the flexibility and power I need.

    How @WithUserDetails works?

    Basically you just need to create a custom UserDetailsService with all the possible users profiles you want to test. E.g

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

    Now we have our users ready, so imagine we want to test the access control to this controller function:

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

    Here we have a get mapped function to the route /foo/salute and we are testing a role based security with the @Secured annotation, although you can test @PreAuthorize and @PostAuthorize as well. Let's create two tests, one to check if a valid user can see this salute response and the other to check if it's actually forbidden.

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

    As you see we imported SpringSecurityWebAuxTestConfig to provide our users for testing. Each one used on its corresponding test case just by using a straightforward annotation, reducing code and complexity.

    Better use @WithMockUser for simpler Role Based Security

    As you see @WithUserDetails has all the flexibility you need for most of your applications. It allows you to use custom users with any GrantedAuthority, like roles or permissions. But if you are just working with roles, testing can be even easier and you could avoid constructing a custom UserDetailsService. In such cases, specify a simple combination of user, password and roles with @WithMockUser.

    @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";
    }
    

    The annotation defines default values for a very basic user. As in our case the route we are testing just requires that the authenticated user be a manager, we can quit using SpringSecurityWebAuxTestConfig and do this.

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

    Notice that now instead of the user manager@company.com we are getting the default provided by @WithMockUser: user; yet it won't matter because what we really care about is his role: ROLE_MANAGER.

    Conclusions

    As you see with annotations like @WithUserDetails and @WithMockUser we can switch between different authenticated users scenarios without building classes alienated from our architecture just for making simple tests. Its also recommended you to see how @WithSecurityContext works for even more flexibility.

    0 讨论(0)
  • 2020-11-28 02:04

    Add in pom.xml:

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <version>4.0.0.RC2</version>
        </dependency>
    

    and use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors for authorization request. See the sample usage at https://github.com/rwinch/spring-security-test-blog (https://jira.spring.io/browse/SEC-2592).

    Update:

    4.0.0.RC2 works for spring-security 3.x. For spring-security 4 spring-security-test become part of spring-security (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, version is the same).

    Setting Up is changed: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())  
                .build();
    }
    

    Sample for basic-authentication: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.

    0 讨论(0)
  • 2020-11-28 02:04

    Here is an example for those who want to Test Spring MockMvc Security Config using Base64 basic authentication.

    String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
    this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
    

    Maven Dependency

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.3</version>
        </dependency>
    
    0 讨论(0)
提交回复
热议问题