SpringSession原理

SpringSession是解决分布式环境下session共享问题的一种方案,它利用Redis作为存储后端。当服务器返回登录成功后的sessionId,浏览器携带此Cookie进行后续请求,服务器通过sessionId获取session信息。在分布式场景下,session复制会带来网络带宽占用和内存浪费问题,而客户端存储又受限于cookie长度和安全性。SpringSession通过装饰模式,创建SessionRepositoryFilter包装request和response,实现了对session的管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Session原理

  • 浏览器第一次访问服务器,登录成功后服务器会保存一个session对象(可看成map对象),session对象里以键值对的方式保存了用户的登录信息,并且每一个session对象都会对应一个独特的sessionId
  • 登录成功后服务器会返回一个JsessionId=xxx的Cookie给浏览器,浏览器下次访问时带上这个cookie,服务器就可以根据sessionid判断用户有没有登录。

session共享问题

  • 服务器发回来的cookie默认作用域domain是本服务器域名,例如让认证服务器返回的是auth.gulimall.com,而cookie作用域不同是不能共享的,也就是说auth.gulimall.com的作用域不能作用于gulimall.com。

  • 分布式下session共享问题,对于集群里的服务器(服务有多个相同的),有以下几种解决方案:

  • 使用springSession解决(底层使用redis)(推荐使用) ,对于不同服务下不用域名的问题,使用自定义cookie的作用域domain解决。

  • session复制,让集群中的服务器互相同步session
    优点:web-server(tomcat)原生支持,只需要修改tomcat配置文件即可
    缺点:1. session同步需要传输数据,占用集群之间的网络带宽
    2.任意一台服务器都需要保存所有服务器的session数据,如果session数据量大的话会导致内存浪费严重,内存限制。
    3.客户端存储,把数据存储在浏览器中的cookie里。
    优点:1. 服务器不需要保存session,节省服务器资源
    缺点:1.不安全,存在泄漏、篡改、窃取等安全问题
    2. 浏览器cookie长度有限制,一般为4k,不同浏览器限制不同,如果session数据过大,浏览器会保存不了

SpringSession原理:

     首先导入 RedisHttpSessionConfiguration 配置类,这个类在容器中添加了 RedisIndexedSessionRepository 组件,	这个组件就是用来存让装饰后的session的bean,相当于是redis操作session的dao[增删改查],同时它又继承了 SpringHttpSessionConfiguration 配置类, 这个类对包装后的Cookie进行了初始化.当服务启动的时候 这个类会自动注入	SessionRepository [我们自己写的配置文件就实现了这个接口]这个组件又在容器中注册了一个springSessionRepositoryFilter 过滤器	;这个过滤器将原生的request、response、session包装成 SessionRepositoryRequestWrapper,以后对session的操作都将在redis进行了

源码一步步分析:

  • @EnableRedi
  • @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(RedisHttpSessionConfiguration.class)
    @Configuration(proxyBeanMethods = false)
    public @interface EnableRedisHttpSession
RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration

这个类是用来配置redis作为存储的分布式session配置类,类似的还有RedissonHttpSessionConfiguration用来做分布式锁

@Bean
	public RedisIndexedSessionRepository sessionRepository

这个配置类向容器中加入了一个RedisIndexedSessionRepository
,这个组件的作用是存储“装饰后的”RedisSessionRepository,相当于操作redis的session dao接口。

  • 同时还继承了SpringHttpSessionConfiguration
  • SpringHttpSessionConfiguration会初始化一个cookie序列化器,这为我们配置自定义cookie做了一个铺垫
@PostConstruct
	public void init() {
		CookieSerializer cookieSerializer = (this.cookieSerializer != null) ? this.cookieSerializer
				: createDefaultCookieSerializer();
		this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
	}
  • 还向容器中注入了一个SessionRepositoryFilter,相当于注入了一个filter
@Bean
	public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
			SessionRepository<S> sessionRepository) {
		SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
		sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
		return sessionRepositoryFilter;
	}

	里面有个方法覆盖了父类filter的doFilterInternal
		@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

		SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
				response);

		try {
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		}
		finally {
			wrappedRequest.commitSession();
		}
	}
	而父类作为一个filter在调用dofilter的时候调用了这个doFilterInternal方法
	@Override
	public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
			throw new ServletException("OncePerRequestFilter just supports HTTP requests");
		}
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;
		String alreadyFilteredAttributeName = this.alreadyFilteredAttributeName;
		boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;

		if (hasAlreadyFilteredAttribute) {
			if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
				doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
				return;
			}
			// Proceed without invoking this filter...
			filterChain.doFilter(request, response);
		}
		else {
			// Do invoke this filter...
			request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
			try {
				doFilterInternal(httpRequest, httpResponse, filterChain);
			}
			finally {
				// Remove the "already filtered" request attribute for this request.
				request.removeAttribute(alreadyFilteredAttributeName);
			}
		}
	}

这相当于将request、response等封装为装饰过后的SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper对象,filter过了之后将包装后的request,response对象传给controller层。
在Controller中想要后的session对象会通过request.getSesison()方法,而这个request对象是包装过后的,之后看看包装后的对象是怎么实现的。

@Override
		public HttpSessionWrapper getSession(boolean create) {
			HttpSessionWrapper currentSession = getCurrentSession();
			if (currentSession != null) {
				return currentSession;
			}
			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 (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;
		}
注意getRequestedSession方法()获取session对象
private S getRequestedSession() {
			if (!this.requestedSessionCached) {
				List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
				for (String sessionId : sessionIds) {
					if (this.requestedSessionId == null) {
						this.requestedSessionId = sessionId;
					}
					S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
					if (session != null) {
						this.requestedSession = session;
						this.requestedSessionId = sessionId;
						break;
					}
				}
				this.requestedSessionCached = true;
			}
			return this.requestedSession;
		}
		之后看sessionRepository.findById(),这里通过调用前面注入的RedisIndexedSessionRepository的findById方法获得session
		@Override
	public RedisSession findById(String id) {
		return getSession(id, false);
	}
	
	private RedisSession getSession(String id, boolean allowExpired) {
		Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
		if (entries.isEmpty()) {
			return null;
		}
		MapSession loaded = loadSession(id, entries);
		if (!allowExpired && loaded.isExpired()) {
			return null;
		}
		RedisSession result = new RedisSession(loaded, false);
		result.originalLastAccessTime = loaded.getLastAccessedTime();
		return result;
	}
	之后调用loadSession方法
	private MapSession loadSession(String id, Map<Object, Object> entries) {
		MapSession loaded = new MapSession(id);
		for (Map.Entry<Object, Object> entry : entries.entrySet()) {
			String key = (String) entry.getKey();
			if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
				loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
			}
			else if (RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY.equals(key)) {
				loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
			}
			else if (RedisSessionMapper.LAST_ACCESSED_TIME_KEY.equals(key)) {
				loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
			}
			else if (key.startsWith(RedisSessionMapper.ATTRIBUTE_PREFIX)) {
				loaded.setAttribute(key.substring(RedisSessionMapper.ATTRIBUTE_PREFIX.length()), entry.getValue());
			}
		}
		return loaded;
	}
	最后返回一个map对象,里面存的数据就是redis保存的session数据。

总结:SpringSession的核心是装饰模式,通过包装一个新的request,response对象去进去操作。

public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter
abstract class OncePerRequestFilter implements Filter 
### Spring与Redis结合实现Session管理的工作原理 在Spring应用中集成Redis作为会话存储解决方案时,`spring-session-data-redis`模块起到了桥梁的作用。通过引入此依赖项并配置相应的设置,应用程序能够利用Redis来持久化HTTP会话数据。 #### RedisHttpSessionConfiguration类的角色 当开发者在项目里添加了`@EnableRedisHttpSession`注解之后,实际上触发了一系列自动配置逻辑,其中最重要的是激活了`RedisHttpSessionConfiguration`类[^3]。该组件负责创建必要的Bean实例用于支持基于Redis的会话处理流程: - **RedisTemplate**: 提供了一种灵活的方式来操作Redis数据库中的键值对。 - **SessionRepositoryFilter**: 这是一个过滤器,在每次请求到达之前都会被调用,用来读取或写回用户的会话信息到Redis服务器上。 ```java @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) { final RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new JdkSerializationRedisSerializer()); template.setHashValueSerializer(template.getValueSerializer()); template.setConnectionFactory(factory); return template; } ``` 上述代码片段展示了如何定义一个连接池工厂bean以及定制化的`RedisTemplate`对象,后者对于序列化/反序列化存放在Redis里的Java对象至关重要。 #### SessionRepository接口的功能描述 在整个架构设计里面,还有一个非常重要的抽象层——即`SessionRepository<T extends ExpiringSession>`接口。它规定了一些基本方法签名以便于具体子类去实现针对不同类型的分布式缓存系统的交互行为。比如保存新的session记录、查找指定id对应的session实体等操作都可以在这个层次得到统一规范[^1]。 ```java public interface SessionRepository<S extends ExpiringSession> { S createSession(); void save(S session); S findById(String id); void deleteById(String id); } ``` 借助这些APIs的支持,开发人员可以更加方便快捷地完成跨多个节点之间的共享状态同步任务而无需关心底层细节。 #### 序列化机制的重要性 值得注意的是,在将复杂的业务对象放入Redis之前通常还需要经历一次转换过程—也就是所谓的“序列化”。默认情况下,Spring Session会选择采用标准JDK所提供的工具来进行这项工作;不过为了提高性能表现或者满足特定需求,则允许替换为其他更高效的方案(如JSON格式)。这一步骤确保即使是在网络传输过程中也能保持原始结构不变从而便于后续恢复使用[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值