在部署为启动独立 WAR(不在应用程序服务器中)的应用程序中使用 Spring Boot (3.2),是否可以控制哪些请求重置会话的不活动状态?
场景: 混合了页面 (JSP) 和 API 的应用程序。我们希望在每个page请求时重置会话不活动状态,但对于API请求不。换句话说,对于坐在定期发出 API 请求(轮询)的页面上的用户,用户的会话永远不会超时。我想限制会话不活动,以便它不会被这些 API 请求重置(因此坐在该页面上的用户会超时)。
这可能吗?如果是这样,怎么办?或者,指向执行不活动重置的 servlet 过滤器的指针也会很有用。
正如评论中提到的,Spring 不参与空闲会话跟踪或超时,它只是将配置传递到嵌入式 Web 服务器(默认为 Tomcat)。
为了实现这一点,我构建了以下类。它的工作原理是将请求包装到“正常”页面(“应该”重置会话超时的请求),以便它可以观察对会话的访问并将该时间记录为会话中的单独属性。对于不应该计入会话活动的请求(由本解决方案中的一组 URL 模式指定),它会根据当前时间检查存储的属性,以查看超时是否已过;如果是这样,它会使会话无效并将请求传递到链上进行处理(这允许 Spring 像任何其他过期/无效会话一样处理它)。
import static java.lang.System.currentTimeMillis;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class IdleSessionTrackingFilter extends OncePerRequestFilter {
private static final String LAST_ACCESS_ATTRIBUTE = "lastAccessTime";
private final Duration idleSessionTimeout;
private final List<AntPathRequestMatcher> matchers;
public IdleSessionTrackingFilter(Duration idleSessionTimeout, String... patterns) {
this.idleSessionTimeout = idleSessionTimeout;
this.matchers = Arrays.stream(patterns)
.map(AntPathRequestMatcher::antMatcher)
.toList();
}
public FilterRegistrationBean<IdleSessionTrackingFilter> reigstrationBean() {
return new FilterRegistrationBean<>(this);
}
protected boolean shouldNotKeepAlive(HttpServletRequest request) throws ServletException {
return matchers.stream().anyMatch(m -> m.matches(request));
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
boolean normalRequest = !shouldNotKeepAlive(request);
if (normalRequest) {
log.debug("Non-matching request, processing as normal.");
// Wrap the request (to observe session access) and process as normal
request = new SessionAccessAwareRequest(request);
chain.doFilter(request, response);
return;
}
HttpSession session = request.getSession(false);
if (session == null) {
log.debug("No session in request to {} {}, processing as normal.", request.getMethod(), request.getRequestURI());
} else {
if (isExpired(session)) {
log.info("Non-renewing request to {} {} and session is old, invalidating it.", request.getMethod(), request.getRequestURI());
session.invalidate();
}
}
chain.doFilter(request, response);
}
protected boolean isExpired(HttpSession session) {
Long lastAccessTime = (Long) session.getAttribute(LAST_ACCESS_ATTRIBUTE);
if (lastAccessTime == null ) {
return false;
}
long idleTime = currentTimeMillis() - lastAccessTime.longValue();
return idleTime > idleSessionTimeout.toMillis();
}
/**
* Request wrapper that observes session access via getSession() method and
* stores that as our internal last-accessed-time.
*/
private static class SessionAccessAwareRequest extends HttpServletRequestWrapper {
public SessionAccessAwareRequest(HttpServletRequest request) {
super(request);
}
@Override
public HttpSession getSession(boolean create) {
HttpSession session = super.getSession(create);
if (session != null) {
session.setAttribute(LAST_ACCESS_ATTRIBUTE, currentTimeMillis());
}
return session;
}
}
}
使用示例:
private static final String ALL_APIS_PATTERN = "/**/api/**";
@Value("${server.servlet.session.timeout}")
private Duration sessionTimeout;
// ...
@Bean
FilterRegistrationBean<IdleSessionTrackingFilter> apiRequestSessionKeepAliveFilter() {
return new IdleSessionTrackingFilter(sessionTimeout, ALL_APIS_PATTERN)
.reigstrationBean();
}
这个解决方案大致基于@pavel-horal 的类似问题的
这个答案