1、分布式架构下的 session 共享问题
2、springboot 整合 spring session 的整合过程
3、简读 Spring Session 源码
1、分布式架构下的 session 共享问题 <--返回目录
1.1、session 的作用:
因为 HTTP 是无状态的协议,web 服务器为了区分记住用户的状态,会为每个用户创建一个会话,存储用户的相关信息,以便在后面的请求中,可以定位到同一个上下文。
例如用户在登陆之后,在进行页面跳转的时候,存储在 session 中的信息会一直保持,如果用户还没有 session,那么服务器会创建一个 session 对象,直到会话过期或主动放弃(退出),服务器才会把 session 终止掉。
配合客户端(浏览器)的使用,一般会使用 cookie 来管理 session。
1.2、分布式架构中的 session 问题
单服务器架构下,session 直接保存在服务器中,是一点问题都没有的。随着分布式架构的流行,单个服务器已经不能满足系统的需要了,通常都会把系统部署多个实例,通过负载均衡把请求分发到其中的一个实例上。这样同一个用户的请求可能被分发到不同的实例上,比如第一次请求访问实例 A,创建了 session,但是下一次访实例 B,这个时候就会出现取不到 session 的情况。于是,分布式架构中,session 共享就成了一个很大的问题。
1.3、分布式架构下的 session 共享问题的解决方案
1)不要有 session:大家可能觉得我说了句废话,但是确实在某些场景下,是可以没有 session 的,其实在很多接口类系统当中,都提倡【API无状态服务】;也就是每一次的接口访问,都不依赖于 session、不依赖于前一次的接口访问;
- 不用 session,比如可以使用 token;
2)存入 cookie 中:将 session 存储到 cookie 中,但是缺点也很明显,例如每次请求都得带着 session,数据存储在客户端本地,是有风险的;
- 即把用户信息等数据直接存到 cookie,这样显然是不安全的;
3)session 同步:对个服务器之间同步session,这样可以保证每个服务器上都有全部的session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
4)使用Nginx(或其他负载均衡软硬件)中的ip绑定策略,同一个ip只能在指定的同一个机器访问,但是这样做风险也比较大,而且也是去了负载均衡的意义;
5)我们现在的系统会把 session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的:实现session共享,可以水平扩展(增加Redis服务器),服务器重启session不丢失(不过也要注意session在Redis中的刷新/失效机制),不仅可以跨服务器session共享,甚至可以跨平台(例如网页端和APP端)。
下面介绍上面解决方案 5 的一个实现:使用 Spring Session。
2、springboot 整合 spring session 的整合过程 <--返回目录
参考:SpringBoot 2 整合 Spring Session 最简操作
测试结果:
1)访问 http://localhost:8080/demo/add/username/zs,向 session 中添加属性 username=zs
2) 访问 http://localhost:8081/demo/get/username, 获取 session 中属性 username
可以看到生成的 包含 sessionId 的 cookie 的 path是 “/项目名”。所以,如果两个项目的项目名不同,则 cookie 不能传递过去。这时需要自定义配置 cookie 的 path。
3、简读 Spring Session 源码 <--返回目录
Spring Session 原理是:实现一个过滤器,将 原生的 request, response, session 等进行装饰,并通过 "filterChain.doFilter(wrappedRequest, wrappedResponse);" 进行掉包,从而开发者在程序中得到的 request, response, session 都是调包后的装饰对象。
// 过滤器 SessionRepositoryFilter { @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 { // write the session id to the response and persist the Session wrappedRequest.commitSession(); } } } // HttpSessionWrapper#commitSession() // write the session id to the response and persist the Session private void commitSession() { HttpSessionWrapper wrappedSession = getCurrentSession(); if (wrappedSession == null) { if (isInvalidateClientSession()) { SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response); } } else { S session = wrappedSession.getSession(); clearRequestedSessionCache(); SessionRepositoryFilter.this.sessionRepository.save(session); String sessionId = session.getId(); if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) { // 写 cookie 到 client SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId); } } } // 写 cookie 到 client public class DefaultCookieSerializer implements CookieSerializer { @Override public void writeCookieValue(CookieValue cookieValue) { HttpServletRequest request = cookieValue.getRequest(); HttpServletResponse response = cookieValue.getResponse(); StringBuilder sb = new StringBuilder(); sb.append(this.cookieName).append('='); String value = getValue(cookieValue); if (value != null && value.length() > 0) { validateValue(value); sb.append(value); } int maxAge = getMaxAge(cookieValue); if (maxAge > -1) { sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge()); ZonedDateTime expires = (maxAge != 0) ? ZonedDateTime.now(this.clock).plusSeconds(maxAge) : Instant.EPOCH.atZone(ZoneOffset.UTC); sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)); } String domain = getDomainName(request); if (domain != null && domain.length() > 0) { validateDomain(domain); sb.append("; Domain=").append(domain); } String path = getCookiePath(request); if (path != null && path.length() > 0) { validatePath(path); sb.append("; Path=").append(path); } if (isSecureCookie(request)) { sb.append("; Secure"); } if (this.useHttpOnlyCookie) { sb.append("; HttpOnly"); } if (this.sameSite != null) { sb.append("; SameSite=").append(this.sameSite); } response.addHeader("Set-Cookie", sb.toString()); } }
"HttpSession session = request.getSession(); " 的底层实现过程:
// 创建session // 类型是 SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper SessionRepositoryRequestWrapper extends HttpServletRequestWrapper { @Override public HttpSessionWrapper getSession() { return getSession(true); } @Override public HttpSessionWrapper getSession(boolean create) { HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } S requestedSession = getRequestedSession(); // 这里代码省略。。。 if (!create) { return null; } // session 底层结构:MapSession S session = SessionRepositoryFilter.this.sessionRepository.createSession(); session.setLastAccessedTime(Instant.now()); // 使用 HttpSessionWrapper 包装 MapSession currentSession = new HttpSessionWrapper(session, getServletContext()); setCurrentSession(currentSession); return currentSession; } } // session 的底层结构:MapSession public class RedisIndexedSessionRepository { @Override public RedisSession createSession() { MapSession cached = new MapSession(); if (this.defaultMaxInactiveInterval != null) { cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval)); } RedisSession session = new RedisSession(cached, true); session.flushImmediateIfNecessary(); return session; } }
"session.setAttribute(name, value);" 的底层实现过程:
// 设置属性 class HttpSessionAdapter<S extends Session> implements HttpSession { @Override public void setAttribute(String name, Object value) { checkState(); Object oldValue = this.session.getAttribute(name); // this.session.setAttribute(name, value); // 这里代码省略。。。 } } RedisIndexedSessionRepository { @Override public void setAttribute(String attributeName, Object attributeValue) { // 这个 cached 就是底层结构 MapSession this.cached.setAttribute(attributeName, attributeValue); this.delta.put(getSessionAttrNameKey(attributeName), attributeValue); // 根据配置 spring.session.redis.flush-mode=on_save/immediate 判断 flushImmediateIfNecessary(); } private void flushImmediateIfNecessary() { if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) { save(); } } private void save() { saveChangeSessionId(); saveDelta(); } /** * Saves any attributes that have been changed and updates the expiration of this * session. */ 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; RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this); } }
---