Session默认是服务器里面一片内存空间,可以当作一个map
session共享问题
1.不能跨不同域名进行共享。(不同服务, 假设是会员服务和订单服务部署在了不同域名。不同域名下jsessionid不能共享)
2.同域名下也会无法共享。(会员服务1,会员服务2。。。。多台服务器同时会有会员服务。)
浏览器客户端登录在会员服务1号服务器,1号服务器在自己的内存空间保存了用户信息,但是由于是分布式集群环境,下一次再进来负载均衡到2号服务器,不知道第1个服务的内存里面的用户信息。你即使带了正确的jsessionid也无法找到这个数据。
即存在2个问题:
-
同域名下,服务有多份,session不同步问题。
-
不同服务,session不能共享问题
分布式session解决方案
方案一:session复制
也称session同步方案,让两个服务器之间互相同步session
优点:tomcat原生支持,只需修改配置文件即可实现。
缺点:session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力。
1.延迟问题
2.占用带宽
3.假设有100台,每个都保存全量数据,大型集群情况下该方案不可取
4.内存限制
方案二:客户端存储
服务器不存储session,用户保存自己的session信息到cookie中。节省服务端资源
缺点:
1.都是缺点,仅仅是一种思路。
2.占用带宽
3.cookie有长度限制4K
4.关键数据容易被篡改
方案三:hash一致性
利用ip的哈希一致性,只要是同一个ip访问的,就一直定位到同一个服务器。
优点:只需修改nginx配置,可以支持服务器的水平扩展。
缺点:
- 服务器重启后导致部分session丢失。
- 如果服务器水平扩展,rehash后session重新分布,会有一部分用户找不到正确的session,但并不影响,session本身就是有有效期的。
方案四:统一存储
后端redis或其他nosql中间件统一存储session
优点:无安全隐患,可水平扩展,重启服务器session不会丢失(因为session都是在redis里存着跟业务服务器宕机与否是没关系的)。
不足:
1.增加了一次网络调用,redis获取数据比内存慢很多。
但这些不足可以用SpringSession解决。
接下来就采用方案四解决该问题。
不同服务,子域session共享问题
**问题引入:**比如用户在 auth.server.com登录,获得的session的domain域中的内容显示的就是auth.server.com,如果需要放大该session的domain到 .server.com 下,如何去做?
问题解析:
为了解决在子域之间 server.com , a.server.com , b.server.com 无论访问哪一个网站,session都能同步过去,需要在auth.server.com发sessionid给用户的时候,也设置让该session也能在 .server.com可以使用。
以后再访问 server.com 时,在请求头(header)中会带上cookie : jessionid=xxxx的信息
SpringSession
SpringSession整合redis文档:https://docs.spring.io/spring-session/reference/guides/boot-redis.html
根据官方文档:
**第一步:**添加依赖
<!--SpringSession-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
**第二步:**修改配置文件
# Session保存类型
spring.session.store-type=redis
更多配置
# session过期时间,默认30分钟
server.servlet.session.timeout=30m
# Session刷新模式.
spring.session.redis.flush-mode=on_save
# Session命名空间
spring.session.redis.namespace=spring:session
**第三步:**配置Redis的连接信息
spring:
redis:
host: 192.168.10.100
port: 6379
Sevlet容器初始化原理
Our Spring Boot Configuration created a Spring bean named springSessionRepositoryFilter that implements Filter. The springSessionRepositoryFilter bean is responsible for replacing the HttpSession with a custom implementation that is backed by Spring Session.
In order for our Filter to do its magic, Spring needs to load our Config class. Last, we need to ensure that our servlet container (that is, Tomcat) uses our springSessionRepositoryFilter for every request. Fortunately, Spring Boot takes care of both of these steps for us.
就是说SpringSession将原生的session替换成自定义session的实现(即用redis来存储)
第四步:在主类上添加注解
@EnableRedisHttpSession
然后开始测试。
当登录成功后,会在session保存loginUser信息
session.setAttribute("loginUser", data);
但这里需要注意data这个属于内存对象,将内存对象远程保存到redis服务器里,需要先把内存对象序列化为二进制流、串。(默认使用的是JDK序列化,需要实现Serializable接口)
public class xxxRespVo implements Serializable {}
启动服务器,测试登录,成功后,在redis中会存入session
在需要取出session的服务,也加上依赖,按照之前的配置步骤同样操作整合springsession
【手动修改】修改session的domain为.server.com,就可以去主页获取用户session
然后就可以在页面上取出session.loginUser.nickname 获得用户的用户名
解决子域session共享问题
在需要共享session的每个服务都可以编写配置类,设置domain名和cookie名
@Configuration
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("server.com");
cookieSerializer.setCookieName("SSERVERSESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
GenericJackson2JsonRedisSerializer
配置实现json存储到redis
SpringSession的核心原理
进入注解:@EnableRedisHttpSession
SpringSession的核心:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
wrappedRequest.commitSession();
}
}
包装原生Request
SessionRepositoryFilter<S.>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
包装原生Response
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
包装后的对象放到filterChain,执行
filterChain.doFilter(wrappedRequest, wrappedResponse);
调用获取Session必须执行这段代码
HttpSession session = request.getSession();
原生的request的对象和response对象都被包装成:
SessionRepositoryRequestWrapper
SessionRepositoryResponseWrapper
因此以后调用request.getSession(),就会去调用wrappedRequest.getSession()
wrappedRequest的getSession源码:
关注这一行
S requestedSession = getRequestedSession();
@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.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
}
....
}
getRequestedSession()
里面调用了 this.sessionRepository
因此就采用了sessionRepository中的对redis进行增删改查的操作
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;
}
主要流程是:原生请求request去获取session被SpringSession采用装饰者模式包装成…sessionWrapper,并传入filterChain执行
springsession具有自动延期功能,只要浏览器不关session会续期。关了浏览器,30分钟一到就会清除。