session是用来在服务器端保存用户登录信息的KV结构数据,用户在浏览器登录之后,服务器端生成sessionId,返回给浏览器端,浏览器下一次请求在Header的Cookie中带上sessionId,服务器根据sessionId就可以获取用户的信息,从而进行登录鉴权等操作。如果sessionId不存在,服务器端会返回一个新的sessionId。
在多个微服务部署的场景中,用户登录之后,从服务A生成session, 拿到sessionId,去请求服务B,服务B没有这个session, 就会出现鉴权不通过的情况。这时需要引入分布式session, 常见的是基于redis 的分布式session
需要引入一下依赖:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
核心注解是@EnableRedisHttpSession, 该注解定义了session有效时间、命名空间、刷新模式、定时清理cron表达式、保存模式等属性
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
// 过期时间,单位秒,默认30min
int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
// session命名空间,一应用一个,多个应用使用同一个redis,需要保持唯一
String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
// redis session刷新模式
@Deprecated
RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
// redis session刷新模式默认是ON_SAVE,即调用SessionRepository#save(Session)之后才会刷新redis,IMMEDIATE:任何对session的更新都会刷新到redis
FlushMode flushMode() default FlushMode.ON_SAVE;
// session过期定时任务的cron表达式,默认每分钟运行
String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
// session的保存模式,默认ON_SET_ATTRIBUTE:只保存对session的修改
SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
}
使用时可以新建配置文件,写入如下的内容(假设已经配置好redis,并使用lettuce)
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600, redisNamespace = "xlt_namespace")
public class RedisSessionConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
}
核心配置类是RedisHttpSessionConfiguration,redis 的session配置需要的信息主要是通过这个类完成。首先,setImportMetadata方法会根据EnableRedisHttpSession注解的配置,读取到spring session的相关配置信息
@Override
@SuppressWarnings("deprecation")
public void setImportMetadata(AnnotationMetadata importMetadata) {
// 获取EnableRedisHttpSession注解的配置信息
Map<String, Object> attributeMap = importMetadata
.getAnnotationAttributes(EnableRedisHttpSession.class.getName());
// 拿到配置属性
AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
// session失效时间
this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds");
// 命名空间
String redisNamespaceValue = attributes.getString("redisNamespace");
if (StringUtils.hasText(redisNamespaceValue)) {
this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue);
}
// 刷新模式
FlushMode flushMode = attributes.getEnum("flushMode");
RedisFlushMode redisFlushMode = attributes.getEnum("redisFlushMode");
if (flushMode == FlushMode.ON_SAVE && redisFlushMode != RedisFlushMode.ON_SAVE) {
flushMode = redisFlushMode.getFlushMode();
}
this.flushMode = flushMode;
// 保存模式
this.saveMode = attributes.getEnum("saveMode");
// session清理定时任务频率配置,cron表达式,默认是每分钟一次 "0 * * * * *"
String cleanupCron = attributes.getString("cleanupCron");
if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron;
}
}
然后是创建RedisIndexedSessionRepository,他主要是管理RedisSession对Redis数据库的操作
@Bean
public RedisIndexedSessionRepository sessionRepository() {
// 创建redisTemplate,用来操作redis,默认使用hset结构存储session信息
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
// 设置应用时间发布器
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
// 设置session最大有效时间
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
// 设置命名空间
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
// 设置刷新模式
sessionRepository.setFlushMode(this.flushMode);
// 设置保存模式
sessionRepository.setSaveMode(this.saveMode);
// 解析数据库类型,0为redis
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
由于RedisHttpSessionConfiguration这个类继承了SpringHttpSessionConfiguration,在setImportMetadata方法完成之后,就会调用springSessionRepositoryFilter方法,新建SessionRepositoryFilter,HTTP的每个请求都要经过这个Filter的处理,校验其中的session信息。
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
SessionRepositoryFilter的doFilter方法实现如下:
@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 设置session属性
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
// 打包请求
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);
// 打包响应
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
// 执行下一个filter
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
// 提交session
wrappedRequest.commitSession();
}
}
}
然后初始化Redis消息监听器容器RedisMessageListenerContainer
@Configuration(proxyBeanMethods = false)
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
//设置redis连接工厂
container.setConnectionFactory(this.redisConnectionFactory);
// 设置任务执行器
if (this.redisTaskExecutor != null) {
container.setTaskExecutor(this.redisTaskExecutor);
}
// 设置redis脚本执行器
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
// 添加消息监听器,监听session的删除、到期、创建事件
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
container.addMessageListener(sessionRepository,
Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
return container;
}
}
接着开启Redis的键空间通知用来确保session的删除和到期会触发session销毁事件
@Configuration(proxyBeanMethods = false)
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
@Bean
public InitializingBean enableRedisKeyspaceNotificationsInitializer() {
//开启Redis的键空间通知
return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction);
}
}
最后设置session事件监听适配器
@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
}
配置了spring session之后,HTTP的每个请求都要经过SessionRepositoryFilter的处理,校验其中的session信息, 他的doFilterInternal方法定义如下:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 设置session属性
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
// 打包请求
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);
// 打包响应
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
// 执行下一个filter
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
// 提交session
wrappedRequest.commitSession();
}
}
其中,commitSession是进行session校验和持久化的关键
// 使用HttpSessionIdResolver把sessionId写入响应并且持久化session
private void commitSession() {
// 获取当前的包装session
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
} else {
// 获取session
S session = wrappedSession.getSession();
// 清理请求session缓存,主要是把请求的sessionId等清空
clearRequestedSessionCache();
// 保存session
SessionRepositoryFilter.this.sessionRepository.save(session);
// 获取sessionId
String sessionId = session.getId();
// 已请求的sessionId是否有效,并且是否和当前的sessionId
if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) { // 不相等
// 生成新的sessionId返回
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
上面的保存session,会调用RedisIndexedSessionRepository的保存方法,将session信息保存到redis
@Override
public void save(RedisSession session) {
// 保存session
session.save();
if (session.isNew) { // 新的session
// 创建session缓存的Key
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
// 保存到redis
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.isNew = false;
}
}
private void save() {
// 保存变更的sesssionId
saveChangeSessionId();
// 保存属性
saveDelta();
}
private void saveChangeSessionId() {
// 获取当前的sessionId
String sessionId = getId();
if (sessionId.equals(this.originalSessionId)) { // sessionId没有变
return;
}
if (!this.isNew) { // sessionId变了
// 原始key
String originalSessionIdKey = getSessionKey(this.originalSessionId);
// 新的key
String sessionIdKey = getSessionKey(sessionId);
try {
// 更新redis中的值
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
// 原始过期key
String originalExpiredKey = getExpiredKey(this.originalSessionId);
// 新过期key
String expiredKey = getExpiredKey(sessionId);
try {
// 更新过期key
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
}
// 更新key
this.originalSessionId = sessionId;
}
// 保存所有属性的变更并修改过期时间
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(this.delta);
String principalSessionKey = getSessionAttrNameKey(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
// 判断基本命名指数是否发生变化
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(sessionId);
}
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
.add(sessionId);
}
}
this.delta = new HashMap<>(this.delta.size());
// 过期时间=上一次访问时间+最大过期时间间隔
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
// 持久化到redis
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}