【精选】spring-session的使用及其原理——分布式session解决方案

文章目录

为什么要用springsession

为了解决分布式系统下session无法共享的问题。

spring-session是spring旗下的一个项目,把servlet容器实现的httpSession替换为spring-session,专注于解决 session管理问题。可简单快速且无缝的集成到我们的应用中。

使用spring-session

pom

xml 复制代码
<!-- 整合springsession -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

配置

java 复制代码
spring:
  session:
    store-type: redis
    redis:
      host: 192.168.1.13
      port: 6379
server:
  servlet:
    session:
      timeout: 30m

要配置redis的连接信息

开启springsession

在配置类或者启动类中开启session共享

java 复制代码
@EnableRedisHttpSession     //整合Redis作为session存储

使用session

正常使用session,就会将session存入redis(data要实现Serializable)

java 复制代码
session.setAttribute(key,data);

修改json方式序列化和作用域

java 复制代码
@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        //放大作用域
        cookieSerializer.setDomainName("cxf.com");
        cookieSerializer.setCookieName("CXFSESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

springsession核心原理(2.6.0版本的springsession,与之前版本可能有出入,但是原理相同)

RedisIndexedSessionRepository

使用@EnableRedisHttpSession注解就会导入RedisHttpSessionConfiguration配置类,其中有一个Bean,就是用来操作session各种增删改查的类。

java 复制代码
@Bean
public RedisIndexedSessionRepository sessionRepository() {
	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);
	}
	sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
	if (StringUtils.hasText(this.redisNamespace)) {
		sessionRepository.setRedisKeyNamespace(this.redisNamespace);
	}
	sessionRepository.setFlushMode(this.flushMode);
	sessionRepository.setSaveMode(this.saveMode);
	int database = resolveDatabase();
	sessionRepository.setDatabase(database);
	this.sessionRepositoryCustomizers
			.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
	return sessionRepository;
}

SessionRepositoryFilter

Spring Session 的核心就是这个 SessionRepositoryFilter。

它包装request和request,后续使用的request就不是原生的request了,而是包装好的,此时调用getSession方法返回的也是包装好的session,就可以直接操作Redis了。

java 复制代码
@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,后续使用的request和response就是包装好的了
	SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
	SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
			response);

	try {
		filterChain.doFilter(wrappedRequest, wrappedResponse);
	}
	finally {
		wrappedRequest.commitSession();
	}
}

SessionRepositoryRequestWrapper

我们可以看到,使用包装的request获取的session,也是一个包装好的session------HttpSessionWrapper,最终实现对session的增删改查是在RedisIndexedSessionRepository类中实现的。

java 复制代码
@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) {
		if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
			requestedSession.setLastAccessedTime(Instant.now());
			this.requestedSessionIdValid = true;
			currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
			currentSession.markNotNew();
			setCurrentSession(currentSession);
			return currentSession;
		}
	}
	else {
		// This is an invalid session id. No need to ask again if
		// request.getSession is invoked for the duration of this request
		if (SESSION_LOGGER.isDebugEnabled()) {
			SESSION_LOGGER.debug(
					"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
		}
		setAttribute(INVALID_SESSION_ID_ATTR, "true");
	}
	if (!create) {
		return null;
	}
	if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver
			&& this.response.isCommitted()) {
		throw new IllegalStateException("Cannot create a session after the response has been committed");
	}
	if (SESSION_LOGGER.isDebugEnabled()) {
		SESSION_LOGGER.debug(
				"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
						+ SESSION_LOGGER_NAME,
				new RuntimeException("For debugging purposes only (not an error)"));
	}
	S session = SessionRepositoryFilter.this.sessionRepository.createSession();
	session.setLastAccessedTime(Instant.now());
	currentSession = new HttpSessionWrapper(session, getServletContext());
	setCurrentSession(currentSession);
	return currentSession;
}

HttpSessionIdResolver

java 复制代码
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

原理简要总结

当请求进来的时候,SessionRepositoryFilter 会先拦截到请求,将 request 和 response 对象转换成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 。后续当第一次调用 request 的getSession方法时,会调用到 SessionRepositoryRequestWrapper 的getSession方法。这个方法是被从写过的,逻辑是先从 request 的属性中查找,如果找不到;再查找一个key值是"SESSION"的 Cookie,通过这个 Cookie 拿到 SessionId 去 Redis 中查找,如果查不到,就直接创建一个RedisSession 对象,同步到 Redis 中。

说的简单点就是:拦截请求,将之前在服务器内存中进行 Session 创建销毁的动作,改成在 Redis 中创建。

站在巨人的肩膀上

blog.csdn.net/weixin_4218...

相关推荐
0zxm21 分钟前
06 - Django 视图view
网络·后端·python·django
m0_7482571822 分钟前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
小_太_阳1 小时前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师1 小时前
Spring基础分析13-Spring Security框架
java·后端·spring
搬码后生仔3 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱3 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
Lx3524 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生4 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
百罹鸟4 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
小蜗牛慢慢爬行5 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate