如何为 Spring 会话设置绝对会话超时

How to set absolute session timeout for a Spring Session

根据 OWASP 会话必须有一个绝对超时,它定义了会话可以处于活动状态的最长时间。我知道如何使用 server.servlet.session.timeout 设置 spring 会话的最大不活动超时,但是我不确定如何设置会话的绝对超时。我想我可以为 Cookie 设置 Max-Age 属性,这可能会用作绝对超时,但是我想知道是否可以在服务器端会话上以某种方式设置绝对超时?

此功能未在 Spring 会话中实现。 有关解决方法,请参阅 https://github.com/spring-projects/spring-session/issues/922

没有 built-in 方法可以使用 Spring 会话。但是,有一些解决方法。

一个解决方案是围绕存储库创建一个 cglib 代理。 这是一个基于JdbcIndexedSessionRepository 的示例,但应该相对于将它扩展到任何其他会话存储库。

需要注意的是,如果 SessionRepository super class 没有默认构造函数,cglib 要求您为构造函数传入参数。在 JdbcIndexedSessionRepository 的情况下,参数需要是 non-null,但它们是接口并且 Spring 提供了易于构造的实现。我本可以使用反射从现有存储库中获取对象,但没有必要这样做,因为代理的 superclass 从未实际使用过。

@Configuration
class SessionConfiguration {

    public static final Duration ABSOLUTE_SESSION_TIMEOUT = Duration.of(8, ChronoUnit.HOURS);

    /**
     * Configures the session repository to have all sessions timeout after at least
     * {@link SessionConfiguration#ABSOLUTE_SESSION_TIMEOUT}.
     *
     * Requires the session repository to be a JdbcIndexedSessionRepository.
     */
    @Bean
    public static BeanPostProcessor absoluteSessionRepositoryBeanPostProcessor() {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull String beanName) {
                if (!(bean instanceof JdbcIndexedSessionRepository)) {
                    return bean;
                }

                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(JdbcIndexedSessionRepository.class);
                enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
                    if (!method.getName().equals("findById")) {
                        return method.invoke(bean, args);
                    }

                    Session session = (Session) method.invoke(bean, args);
                    if (session == null) {
                        return null;
                    }

                    Instant timeoutTime = session.getCreationTime().plus(ABSOLUTE_SESSION_TIMEOUT);
                    if (timeoutTime.compareTo(Instant.now()) < 0) {
                        return null;
                    }

                    return session;
                });

                return enhancer.create(
                        new Class[]{JdbcOperations.class, TransactionOperations.class},
                        new Object[]{new JdbcTemplate(), new TransactionTemplate()}
                );
            }
        };
    }

}

另一种解决方案是使用 SessionRepositoryCustomizer 来更改存储库调用的 SQL 查询。当然,这仅在您的存储库基于 SQL 数据库 时有效,例如 JdbcIndexedSessionRepository。 此代码使用 MySQL 特定行为UNIX_TIMESTAMP() 函数,但将其扩展到大多数其他 SQL 数据库应该相对简单。

@Configuration
class SessionConfiguration {

    public static final Duration ABSOLUTE_SESSION_TIMEOUT = Duration.of(8, ChronoUnit.HOURS);

    /**
     * Configures the session repository to have all sessions timeout after
     * {@link SessionConfiguration#ABSOLUTE_SESSION_TIMEOUT}.
     *
     * Requires the session repository to be a JdbcIndexedSessionRepository and for the database to be MySQL.
     * The customizer sets a custom query that relies on the MySQL-specific UNIX_TIMESTAMP function.
     *
     * UNIX_TIMESTAMP() returns the current time in seconds since the epoch while the session's CREATION_TIME is in
     * milliseconds since the epoch, so we need to multiply the current time by 1000 to get comparable numbers.
     * See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_unix-timestamp for more
     */
    @Bean
    public SessionRepositoryCustomizer<JdbcIndexedSessionRepository> absoluteTimeoutSessionRepositoryCustomizer() {
        return sessionRepository -> {
            String query = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, "
                    + "S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES "
                    + "FROM %TABLE_NAME% S "
                    + "LEFT JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID "
                    + "WHERE S.SESSION_ID = ? AND (UNIX_TIMESTAMP() * 1000 - S.CREATION_TIME) < "
                    + ABSOLUTE_SESSION_TIMEOUT.toMillis();
            sessionRepository.setGetSessionQuery(query);
        };
    }

}