Spring Session 关于 SessionRepositoryFilter SessionRepository HttpSessionIdResolver 等
前言
最近因为项目应用,对 Spring Session
做了些许了解,以此文总结记录
SessionRepositoryFilter
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
// ...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
/**
* 把 HttpServletRequest 和 HttpServletResponse 装饰成 SessionRepositoryRequestWrapper
* 和 SessionRepositoryResponseWrapper,让它们走接下来的 filterChain
*/
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
// ...
}
Spring Session
的核心就是这个SessionRepositoryFilter
- 它把
HttpServletRequest
和HttpServletResponse
装饰成SessionRepositoryRequestWrapper
和SessionRepositoryResponseWrapper
,它们在后续的处理就会接管session
的管理
SessionRepositoryRequestWrapper
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
// ...
@Override
public HttpSessionWrapper getSession(boolean create) {
// 先从 request attribute 获取,同一个 request 的 session 会放在这
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
// 获取不到,就解析 sessionId,然后基于此从 repository 获取
S requestedSession = getRequestedSession();
if (requestedSession != null) {
// 获取到后的一些处理,比如放到上面的 request attribute 里
// ...
}
else {
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
if (!create) {
return null;
}
// create == true 就创建新的 session
// sessionRepository.createSession()
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
private S getRequestedSession() {
if (!this.requestedSessionCached) {
// httpSessionIdResolver.resolveSessionIds
// 基于 httpSessionIdResolver 解析 sessionId,比如从 Cookie RequestHeader
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
for (String sessionId : sessionIds) {
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
// 基于上述 sessionid 获取 Session
S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
// 过期的话调用 httpSessionIdResolver.expireSession
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
}
else {
/**
* sessionRepository.save 保存session
* httpSessionIdResolver.setSessionId 将 sessionId 反馈给 客户端
*/
S session = wrappedSession.getSession();
clearRequestedSessionCache();
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
// ...
}
SessionRepositoryRequestWrapper
是SessionRepositoryFilter
的内部类,装饰HttpSevletRequest
以接管对Session
的管理,几个代表性的方法:getSession
,简单地说,这里会根据客户端请求解析对应解析对应的sessionId
,然后从SessionRepository
获取对应session
,如果获取不到,由SessionRepository
创建新的session
commitSession
方法会把上面创建的session
存储到SessionRepository
SessionRepository
public interface SessionRepository<S extends Session> {
// 创建新的 session
S createSession();
// 保存创建的 session
void save(S session);
// 基于 sessionId 获取对应的 session
S findById(String id);
// 基于 sessionId 删除对应的 session
void deleteById(String id);
}
SessionRepositoryFilter
把对session
的操作就委托到SessionRepository
上了,包括持久化管理
createSession
方法为新的回话创建一个新的session
save
方法把新创建的session
持久化管理起来findById
方法基于解析的sessionId
获取对应的session
,比如从redis
读取deleteById
方法基于解析的sessionId
删除对应的session
- 最常用的实现莫过于基于
redis
的RedisSessionRepository
RedisIndexedSessionRepository
等,后者是使用@EnableRedisHttpSession
注解时默认注册的SessionRepository
,功能更强大
HttpSessionIdResolver
public interface HttpSessionIdResolver {
// 从客户端请求解析 sessionId
List<String> resolveSessionIds(HttpServletRequest request);
// 告诉客户端新创建 session 的 sessionId,比如放到 Cookie 里
void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId);
// 告知客户端 session 过期,比如从 Cookie 里移除对应的 sessionId
void expireSession(HttpServletRequest request, HttpServletResponse response);
}
- 该接口主要负责
sessionId
的解析、处理工作 resolveSessionIds
方法解析客户端请求对应的sessionId
,比如从Cookie
解析、RequestHeader
解析setSessionId
方法将创建的sessionId
返回给客户端,比如放到Cookie
、ResponseHeader
里expireSession
方法告知客户端session
已过期,比如从Cookie
移除、删除对应的ResponseHeader
Spring
提供的实现有CookieHttpSessionIdResolver
和HeaderHttpSessionIdResolver
,前者是基于Cookie
后者基于Header
,默认的是CookieHttpSessionIdResolver
SpringHttpSessionConfiguration
@Configuration(proxyBeanMethods = false)
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
// ...
private CookieHttpSessionIdResolver defaultHttpSessionIdResolver = new CookieHttpSessionIdResolver();
/**
* 注册核心的 SessionRepositoryFilter,指定两大组件:
* sessionRepository
* httpSessionIdResolver
*/
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
// 支持用户自定义 httpSessionIdResolver
@Autowired(required = false)
public void setHttpSessionIdResolver(HttpSessionIdResolver httpSessionIdResolver) {
this.httpSessionIdResolver = httpSessionIdResolver;
}
// ...
}
Spring
提供的配置类,该配置类基于@EnableSpringHttpSession
引入- 注册核心组件
SessionRepositoryFilter
,指定sessionRepository
和httpSessionIdResolver
,其中sessionRepository
自然是由具体的实现决定 httpSessionIdResolver
组件支持覆盖,默认CookieHttpSessionIdResolver
RedisHttpSessionConfiguration
@Configuration(proxyBeanMethods = false)
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
// ...
// 默认创建一个 RedisIndexedSessionRepository,支持事件
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
// ...
/**
* 收集容器中所有的 SessionRepositoryCustomizer<RedisIndexedSessionRepository> 对
* sessionRepository 自定义处理
*/
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
/**
* 收集容器里的 @SpringSessionRedisConnectionFactory 的 RedisConnectionFactory Bean 组件
* 作为 Spring Session 的 RedisConnectionFactory
* 如果没有就降级为全局的 RedisConnectionFactory
* 这个地方旨在使用者定义不同的 redis 数据源,但是实际在整合了 spring-boot-data-redis 的场景里,
* 如果指定了 @SpringSessionRedisConnectionFactory 的 RedisConnectionFactory Bean 组件,
* 则 RedisAutoConfiguration 就不会自动装配 RedisConnectionFactory 了,因此并不好用
*/
@Autowired
public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,
ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory.getIfAvailable();
if (redisConnectionFactoryToUse == null) {
redisConnectionFactoryToUse = redisConnectionFactory.getObject();
}
this.redisConnectionFactory = redisConnectionFactoryToUse;
}
// ...
}
- 该配置类基于
@EnableRedisHttpSession
注解引入,它是SpringHttpSessionConfiguration
的子类,因此核心的组件也都会引入 - 该配置类专注基于
redis
的session
管理,因此注册默认的sessionRepository
为RedisIndexedSessionRepository
- 对应的
redisConnectionFactory
可由@SpringSessionRedisConnectionFactory
注解指定 - 使用该注解且同时整合
spring-boot-data-redis
时不太好用(具体可见注释说明),因此个人偏向于基于@EnableSpringHttpSession
注解引入,即自己创建sessionRepository
、redisConnectionFactory
- 当然如果应用本身不使用
redis
或者不需要与session
分离,则基于@EnableRedisHttpSession
更加方便
SessionAutoConfiguration
// 引入 spring-session 时自动装配
@ConditionalOnClass(Session.class)
@ConditionalOnWebApplication
@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class, WebFluxProperties.class })
public class SessionAutoConfiguration {
// ...
// servlet 环境
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@Import({ ServletSessionRepositoryValidator.class, SessionRepositoryFilterConfiguration.class })
static class ServletSessionConfiguration {
// ...
// ServletSessionConfigurationImportSelector 会引入 sevlet 环境的所有配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(SessionRepository.class)
@Import({ ServletSessionRepositoryImplementationValidator.class,
ServletSessionConfigurationImportSelector.class })
static class ServletSessionRepositoryConfiguration {
}
}
/**
* 根据 web 类型加载对应的配置类
* 比如 servlet 环境下基于 redis 的实现则使用配置类 RedisSessionConfiguration
* 详情:
* private static final Map<StoreType, Configurations> MAPPINGS;
* static {
* Map<StoreType, Configurations> mappings = new EnumMap<>(StoreType.class);
* mappings.put(StoreType.REDIS,
* new Configurations(RedisSessionConfiguration.class, RedisReactiveSessionConfiguration.class));
* mappings.put(StoreType.MONGODB,
* new Configurations(MongoSessionConfiguration.class, MongoReactiveSessionConfiguration.class));
* mappings.put(StoreType.JDBC, new Configurations(JdbcSessionConfiguration.class, null));
* mappings.put(StoreType.HAZELCAST, new Configurations(HazelcastSessionConfiguration.class, null));
* mappings.put(StoreType.NONE,
* new Configurations(NoOpSessionConfiguration.class, NoOpReactiveSessionConfiguration.class));
* MAPPINGS = Collections.unmodifiableMap(mappings);
* }
*/
abstract static class SessionConfigurationImportSelector implements ImportSelector {
protected final String[] selectImports(WebApplicationType webApplicationType) {
return Arrays.stream(StoreType.values())
.map((type) -> SessionStoreMappings.getConfigurationClass(webApplicationType, type))
.toArray(String[]::new);
}
}
static class ServletSessionConfigurationImportSelector extends SessionConfigurationImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return super.selectImports(WebApplicationType.SERVLET);
}
}
// ...
}
Spring Boot
自然也会提供一站式的自动装配
功能- 在我们引入
spring-session
依赖时,该类会自动装配 - 这里比较核心的一点就是通过
ImportSelector
基于web
环境装配对应配置类 - 比如
servlet
环境加载对应的配置类,又根据spring.session.redis.store-type
加载配置类RedisSessionConfiguration
RedisSessionConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RedisTemplate.class, RedisIndexedSessionRepository.class })
// 用户没有注册 SessionRepository 时装配
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@Conditional(ServletSessionCondition.class)
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {
// ...
/**
* 继承 RedisHttpSessionConfiguration,在其基础上允许基于 spring.session.redis
* 前缀指定对应属性
*/
@Configuration(proxyBeanMethods = false)
public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {
@Autowired
public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
Duration timeout = sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}
setRedisNamespace(redisSessionProperties.getNamespace());
setFlushMode(redisSessionProperties.getFlushMode());
setSaveMode(redisSessionProperties.getSaveMode());
setCleanupCron(redisSessionProperties.getCleanupCron());
}
}
}
- 这里的模式,跟
spring-boot-webmvc
模块的自动装配类似,不是直接使用,而是提供拓展的RedisHttpSessionConfiguration
- 提供了
SpringBootRedisHttpSessionConfiguration
,主要是支持基于spring.session.redis
前缀来配置对应的属性 - 不难理解,跟
webMVC
模块的配置一样,如果你的应用使用了类似@EnableSpringHttpSession
@EnableRedisHttpSession
注解,那自动装配就失效了
demo
最后,提供一个自定义数据源的配置示例做个总结
// 基于 SpringHttpSessionConfiguration 拓展
@EnableSpringHttpSession
public class RedisSessionConfig {
/**
* 自定义的 redis 连接属性
*/
@Value("${uasa.redis.host}")
String host;
@Value("${uasa.redis.username:}")
String username;
@Value("${uasa.redis.password:}")
String password;
@Value("${uasa.redis.port}")
int port;
@Value("${uasa.redis.database}")
int database;
@Value("${uasa.redis.maxInactiveIntervalInSeconds}")
int maxInactiveIntervalInSeconds;
@Bean
public SessionRepository sessionRepository() {
// redis 配置
RedisStandaloneConfiguration configuration
= new RedisStandaloneConfiguration();
configuration.setHostName(host);
configuration.setPort(port);
configuration.setUsername(username);
configuration.setPassword(RedisPassword.of(password));
configuration.setDatabase(database);
// connectionFactory 创建
LettuceConnectionFactory connectionFactory
= new LettuceConnectionFactory(configuration);
// 相当于交给 Spring 容器初始化
connectionFactory.afterPropertiesSet();
// RedisTemplate 创建
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 相当于交给 Spring 容器初始化
redisTemplate.afterPropertiesSet();
// SessionRepository 创建
RedisSessionRepository sessionRepository = new RedisSessionRepository(redisTemplate);
// 自定义过期时间
sessionRepository.setDefaultMaxInactiveInterval(
Duration.ofSeconds(maxInactiveIntervalInSeconds)
);
// 自定义 session 前缀
sessionRepository.setRedisKeyNamespace("uasa:session");
return sessionRepository;
}
@Bean
public HttpSessionIdResolver sessionIdResolver() {
// 覆盖默认的 CookieHttpSessionIdResolver
return new HeaderHttpSessionIdResolver("token");
}
}
- 本示例基于
@EnableSpringHttpSession
使用,然后指定自己的SessionRepository
- 这样就不跟
spring-boot-data-redis
自动装配的RedisConnectionFactory
冲突了,可以实现与主业务redis
的完美隔离 - 当然如果主应用不使用或者与
spring-session
使用同一个redis
数据源,则基本不需要配置
总结
spring-session
作为 session
共享的主要 解决方案
之一,进行稍微深入的了解还是很有必要的