如何在 Spring Boot 测试中模拟会话关闭/过期?

Posted

技术标签:

【中文标题】如何在 Spring Boot 测试中模拟会话关闭/过期?【英文标题】:How to simulate session closing / expiring in Spring Boot tests? 【发布时间】:2018-05-01 20:23:47 【问题描述】:

我想在此处显示的示例中添加几个测试:

https://spring.io/guides/gs/securing-web/

能够验证用户在会话关闭或到期时无法再访问需要身份验证的资源。我想在我的测试中模拟以下两种情况:

a) 用户自愿结束会话(例如关闭浏览器);

b) 会话超时;

我不知道如何使用 MockMvc 重现这些情况。

我设法做到了以下几点:

@Test
public void sessionIsInvalid() throws Exception 
    FormLoginRequestBuilder login = formLogin()
            .user("user")
            .password("password");

    mockMvc.perform(login)
            .andExpect(authenticated())
            .andDo(mvcResult -> 
                MockHttpSession session = (MockHttpSession)mvcResult.getRequest().getSession();
                session.invalidate();
                mockMvc.perform(get("/hello")
                        .session(session))
                        .andExpect(status().isFound());
            );

...这似乎可行,但我不完全确定 invalidate 在这种情况下的作用以及它是否符合上述条件 a)。

为了模拟会话超时,我已经这样做了:

@Test
public void sessionExpires() throws Exception 
    FormLoginRequestBuilder login = formLogin()
            .user("user")
            .password("password");

    mockMvc.perform(login)
            .andExpect(authenticated())
            .andDo(mvcResult -> 
                MockHttpSession session = (MockHttpSession)mvcResult.getRequest().getSession();
                session.setMaxInactiveInterval(1);
                Thread.sleep(3);
                mockMvc.perform(get("/hello")
                        .session(session))
                        .andExpect(status().isFound());
            );

...但这不起作用。有人可以帮我理解我做错了什么吗?

【问题讨论】:

【参考方案1】:
session.setMaxInactiveInterval(1);  // in seconds

Thread.sleep(3);  // in milliseconds

【讨论】:

请在您的回答中添加解释【参考方案2】:

当使用带有 Spring Security 的 Spring Boot 时(这就是你的链接),我的方法是这样的:

创建一个自定义 spring 安全过滤器,它能够“说服”spring security session 已过期(无论它认为 session 是什么) 在ConcurrentSessionFilter 之前添加自定义过滤器 创建一个内部静态@TestConfiguration 类,理论上它可以配置HttpSecurity 来添加自定义过滤器(这就是我们想要的)。在实践中,我发现通常我必须用 @TestConfiguration 注释类来扩展我的项目的安全配置类(或者至少是主要的,如果有很多,例如 SecurityConfiguration 用于我的项目);因为在SecurityConfiguration 中我通常也声明其他@Bean(例如CorsConfigurationSource)我通常还必须使用@WebMvcTest(properties = "spring.main.allow-bean-definition-overriding=true", ...) 以避免bean 覆盖错误;用@TestConfiguration 注释的类也用@Order(HIGHEST_PRECEDENCE) 注释。 创建一个简单的 web mvc 测试,尝试 GET 一些项目现有的端点,例如:
@Test
@SneakyThrows
@WithMockUser
void sessionExpired() 
    this.mvc.perform(get("/some-endpoint-here")).andExpect(...);

运行测试并期待您配置的会话过期策略启动;见HttpSecurity.sessionManagement(session -> session...expiredUrl(...))HttpSecurity.sessionManagement(session -> session...expiredSessionStrategy(...))

以下作为 @TestConfiguration 提供的 Spring 安全配置适用于 Spring Boot 2.3.12.RELEASE(可能还有更多)。

@TestConfiguration
@Order(HIGHEST_PRECEDENCE)
static class Config extends SecurityConfiguration 
    public Config(SessionInformationExpiredStrategy expiredSessionStrategy, InvalidSessionStrategy invalidSessionStrategy) 
        super(expiredSessionStrategy, invalidSessionStrategy);
    

    @SneakyThrows
    @Override
    protected void configure(HttpSecurity http) 
        super.configure(http);
        // the custom filter as a lambda expression
        http.addFilterBefore((request, response, chain) -> 
            // preparing some objects we gonna need
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpSession session = httpRequest.getSession(false);
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            // getting our hands on the object that Spring Security believes
            // is the "session" and which is in ConcurrentSessionFilter
            List<Filter> filters = (List) ReflectionTestUtils.getField(chain, "additionalFilters");
            int currentPosition = (int) ReflectionTestUtils.getField(chain, "currentPosition");
            ConcurrentSessionFilter concurrentSessionFilter = (ConcurrentSessionFilter) filters.get(currentPosition);
            SessionRegistry sessionRegistry = (SessionRegistry) ReflectionTestUtils.getField(concurrentSessionFilter, "sessionRegistry");
            // the "session" does not exist (from Spring Security's PoV), 
            // we actually have to create (aka "register") it
            sessionRegistry.registerNewSession(session.getId(), authentication.getPrincipal());
            // the actual session expiration (from Spring Security's PoV)
            sessionRegistry.getSessionInformation(session.getId()).expireNow();
            // let the filters continue their job; ConcurrentSessionFilter
            // follows and it'll determine that the "session" is expired
            chain.doFilter(request, response);
        , ConcurrentSessionFilter.class);
        log.debug("begin");
    

【讨论】:

以上是关于如何在 Spring Boot 测试中模拟会话关闭/过期?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring Boot 中模拟数据库连接以进行测试?

Spring Boot 在关闭时关闭休眠会话 - 在 @Async 方法完成之前

如何确保 SFTP 会话始终在 spring-batch 结束时关闭

Spring Boot 如何处理 Hibernate 会话?

如何在 Spring-boot 中不模拟服务类的情况下为 REST 控制器端点编写单元测试

如何在 Spring Boot REST API 中为 db insert 方法编写模拟单元测试?