使用 Spring Security 进行 Spring Boot 单元测试

Posted

技术标签:

【中文标题】使用 Spring Security 进行 Spring Boot 单元测试【英文标题】:Spring Boot Unit Testing with Spring Security 【发布时间】:2017-10-21 09:24:56 【问题描述】:

我有一个简单的应用程序,我使用自定义 mysql 数据库设置了 Spring Security。现在我正在为它编写测试用例,它们似乎在登录页面和登录后任何有效的东西上都失败了。我的问题是如何为它编写测试用例来检查成功登录和后续请求?

我的安全配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private DataSource dataSource;

    @Value("$spring.queries.users-query")
    private String usersQuery;

    @Value("$spring.queries.roles-query")
    private String rolesQuery;

    @Autowired 
    private CustomAuthenticationSuccessHandler successHandler;

    /** Providing the queries and data source for security*/
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception 
    
        auth.
            jdbcAuthentication()
                .usersByUsernameQuery(usersQuery)
                .authoritiesByUsernameQuery(rolesQuery)
                .dataSource(dataSource)
                .passwordEncoder(bCryptPasswordEncoder);
    

    /** Defining fine grained access for ADMIN and CUSTOMER user */
    @Override
    protected void configure(HttpSecurity http) throws Exception 

        http.
            authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/registration").permitAll()
                .antMatchers("/user/**").hasAuthority(AppRole.CUSTOMER.toString())
                .antMatchers("/health/**").hasAuthority(AppRole.ADMIN.toString())
                .antMatchers("/admin/**").hasAuthority(AppRole.ADMIN.toString()).anyRequest()
                .authenticated().and().csrf().disable().formLogin()
                .loginPage("/login").failureUrl("/login?error=true")
                .successHandler(successHandler)
                .usernameParameter("username")
                .passwordParameter("password")
                .and().logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/").and().exceptionHandling()
                .accessDeniedPage("/access-denied");
    

    /** Defining ant matchers that should ignore the paths and provide no access to any one */
    @Override
    public void configure(WebSecurity web) throws Exception 
    
        web
           .ignoring()
           .antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**");
    


我的自定义成功处理程序:

@Component
@Configuration
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler 

    /** Getting reference to UserService */
    @Autowired
    private UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse, Authentication authentication) 
                    throws IOException, ServletException, RuntimeException 
    
        HttpSession session = httpServletRequest.getSession();
        User authUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        com.crossover.techtrial.java.se.model.User user = userService.findUserByUsername(authUser.getUsername());
        session.setAttribute("userId", user.getUserId());
        session.setAttribute("username", authUser.getUsername());
        session.setAttribute("accountId", user.getAccountId());
        //set our response to OK status
        httpServletResponse.setStatus(HttpServletResponse.SC_OK);
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        authorities.forEach(authority -> 
                                 
                                    if(authority.getAuthority().equals(AppRole.ADMIN.toString())) 
                                     
                                        session.setAttribute("role", AppRole.ADMIN);
                                        try
                                        
                                            //since we have created our custom success handler, its up to us to where
                                            //we will redirect the user after successfully login
                                            httpServletResponse.sendRedirect("/admin/home");
                                         
                                        catch (IOException e) 
                                        
                                            throw new RuntimeException(e);
                                                                                                                   
                                    
                                    else if (authority.getAuthority().equals(AppRole.CUSTOMER.toString()))
                                    
                                        session.setAttribute("role", AppRole.CUSTOMER);
                                        try
                                        
                                            //since we have created our custom success handler, its up to us to where
                                            //we will redirect the user after successfully login
                                            httpServletResponse.sendRedirect("/user/home");
                                         
                                        catch (IOException e) 
                                        
                                            throw new RuntimeException(e);
                                           
                                    
                                );

    


经过一番搜索后,我尝试编写这样的测试用例,但它们似乎不起作用:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class TrialApplicationTests 
    
        @Autowired
        private WebApplicationContext webApplicationContext;

        @Autowired
        private FilterChainProxy springSecurityFilterChain;

        @Autowired
        private MockHttpServletRequest request;

        private MockMvc mockMvc;


        @Test
        public void contextLoads() 
        
        

        @Before
        public void setup() 
        
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                    .addFilters(springSecurityFilterChain)
                    .build();
        
        @Test
        public void verifiesLoginPageLoads() throws Exception 
        
            mockMvc.perform(MockMvcRequestBuilders.get("/"))
                   .andExpect(MockMvcResultMatchers.model().hasNoErrors())
                   .andExpect(MockMvcResultMatchers.view().name("login"))
                   .andExpect(MockMvcResultMatchers.status().isOk());
        

        @Test
        public void testUserLogin()  throws Exception
        
            HttpSession session = mockMvc.perform(post("/login")
                    .contentType(MediaType.APPLICATION_FORM_URLENCODED) 
                    .param("username", "test")
                    .param("password", "test123")
                    )
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    //.andExpect(redirectedUrl("/user/home"))
                    .andReturn()
                    .getRequest()
                    .getSession();

            request.setSession(session);

            SecurityContext securityContext = (SecurityContext)   session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);

            SecurityContextHolder.setContext(securityContext);
        

        @Test
        public void testRetrieveUserBookings() throws Exception
        
            testUserLogin();

            mockMvc.perform(MockMvcRequestBuilders.get("user/bookings"))
                .andExpect(MockMvcResultMatchers.model().hasNoErrors())
                .andExpect(MockMvcResultMatchers.model().attributeExists("bookings"))
                .andExpect(MockMvcResultMatchers.view().name("user/bookings"))
                .andExpect(content().string(containsString("Booking")));
        

    

I searched on the net and there are links WithMockUser and UserDetails, but the problem is as you can see I'm setting a my primary key userId in the session in my custom success handler. So I would also need to get the session in my test. Please tell me the simplest way to write tests that will work, possibly with code since I'm new with security and all such. 

UPDATE:

I changed the code as suggested but still getting the 404 error on my testRetrieveUserBookings. Any more ideas?

@RunWith(SpringRunner.class)
@ContextConfiguration
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@TestExecutionListeners(listeners=ServletTestExecutionListener.class,
        DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        WithSecurityContextTestExecutionListener.class)
public class TrialApplicationTests 

    @Autowired
    private WebApplicationContext webApplicationContext;

    MockMvc mockMvc; 

    @Autowired
    ForestApiClient apiClient;

    @Autowired
    AccountClient accountClient;

    @Autowired
    AirlineClient airlineClient;

    @Autowired
    UserService userService;

    private final String INTEGRATION_ACCOUNT = "account1";

    private MockHttpSession mockSession;

    private Authentication authentication;

    @Test
    public void contextLoads() 
    
    

    @Before
    public void setup() 
    
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                //.addFilters(springSecurityFilterChain)
                .build();   

        mockSession = new MockHttpSession(webApplicationContext.getServletContext(), UUID.randomUUID().toString()); 
        mockSession.setAttribute("userId", 3);
        mockSession.setAttribute("accountId", "ZWR26539");
    

    @Test
    public void testVerifiesLoginPageLoads() throws Exception 
    
        mockMvc.perform(MockMvcRequestBuilders.get("/"))
               .andExpect(MockMvcResultMatchers.model().hasNoErrors())
               .andExpect(MockMvcResultMatchers.view().name("login"))
               .andExpect(MockMvcResultMatchers.status().isOk());
    

    @Test
    public void testRegistration()  throws Exception
    
        mockMvc.perform(post("/registration")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED) 
                .param("username", "test2")
                .param("password", "test123")
                .param("email", "crossovertestuser@gmail.com")
                .param("address", "Some Address")
                .param("accountCurrency", "USD")
                )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.model().hasNoErrors())
                .andExpect(MockMvcResultMatchers.model().attributeExists("user"))
                .andExpect(MockMvcResultMatchers.view().name("registration"))
                .andExpect(content().string(containsString("User has been registered successfully")));
    

    @Test
    @WithMockUser(username="test",roles="USER","ADMIN")
    public void testRetrieveUserBookings() throws Exception
    

        mockMvc.perform(MockMvcRequestBuilders.get("user/bookings"))
            .andExpect(MockMvcResultMatchers.model().hasNoErrors())
            .andExpect(MockMvcResultMatchers.model().attributeExists("bookings"))
            .andExpect(MockMvcResultMatchers.view().name("user/bookings"))
            .andExpect(content().string(containsString("Booking")));
    

【问题讨论】:

【参考方案1】:

如果您的问题只是在测试中获取会话,那么您可以使用 MockHttpSession。

@Before
public void setUp() throws Exception 
    mock = MockMvcBuilders.webAppContextSetup(wac).addFilters(springSecurityFilterChain).build();        
    MockHttpSession httpSession = new MockHttpSession(webAppContext.getServletContext(), UUID.randomUUID().toString());


@Test
public void test1()
    mock.perform(get("/").session(mockSession)).perfor();

【讨论】:

我按照你告诉我的做了,但它似乎没有从会话中选择值。我收到 404 错误。请查看更新代码的编辑。

以上是关于使用 Spring Security 进行 Spring Boot 单元测试的主要内容,如果未能解决你的问题,请参考以下文章

在 Grails 3.0 中配置 Spring Boot Security 以使用 BCrypt 密码编码

Maven:Spring 4 + Spring Security

spring security 没有调用 loadUserByUsername() 方法

Spring:使用Spring Security配置执行器端点的安全性

002 Hello Spring Security

使用 Spring Security 5 OAuth2 实现 Keyclock 授权服务器