由于HTTP协议是无状态的,在开发中我们可以将用户的信息存储在服务器的session中,并生成与之相对应的JSESSIONID通过cookie返回给浏览器。浏览器下次访问,cookie会自动携带上次请求存储的数据(JSESSIONID)到服务器中,服务器根据JSESSIONID找到对应的session,从而获取用户的信息。
该机制在单体应用中是没有问题的,但是如果在分布式环境下,会产生session共享问题,即session的数据在服务1中存在,但是在服务2中不存在。
就会出现下面的问题:
假设用户第一次访问的是会员服务1,会员服务1将用户的信息记录在自己的session中,但是当用户第二次访问的是会员服务2时,就会找不到用户信息
服务器将自己的session数据传送给其他服务器,使得每个服务器都拥有全量的数据。
优点:tomcat原生支持,只需要修改配置文件即可
缺点:
用户的信息不再保存在服务器中,而是保存在客户端(浏览器)中。
优点:服务器不需要保存用户信息,节省服务器资源
缺点:
一般情况下不会使用这种方案。
nginx负载均衡的时候采用ip-hash策略,这样同一个客户端每次的请求都会被同一个服务器处理。
优点:
缺点:
但以上缺点其实问题不大,因为session本来也是有有效期的,所以这个方案也经常被采用。
jsessionid这个cookie默认是系统域名。当我们分拆服务,不同域名部署的时候,我们可以使用如下解决方案。
将用户的信息存储在第三方中间件上,做到统一存储,如redis中,所有的服务都到redis中获取用户信息,从而实现session共享。
优点
不足:
现在我们知道,我们可以将session的信息存储在第三方数据库中,比如redis。但如果是我们自己去写这个逻辑的话太过麻烦。而spring session可以很简单的帮助我们实现这个功能。
官网地址:spring session官网地址
1、添加依赖
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
2、配置文件添加配置
spring.session.store-type=redis
3、配置redis连接
spring.redis.host=localhost # Redis server host.
spring.redis.password= # Login password of the redis server.
spring.redis.port=6379 # Redis server port.
4、使用json序列化机制
@Configuration
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericFastJsonRedisSerializer();
}
}
spring session默认是jdk序列化机制,要求类需要实现Serializable接口,序列化后是二进制,人看不懂。使用json序列化机制就没有这些问题。
5、在springboot启动类中添加@EnableRedisHttpSession
注解
这样就OK了
session不能跨不同域名共享
当我们的认证微服务以及其他微服务使用的是俩个不同的域名时,即使使用了spring session也会存在不同域名的共享问题。
比如,认证服务的域名为auth.fcpmall.com,订单服务的域名为
order.fcpmall.com,这种情况下,即使在认证服务登录成功,将用户的信息保存在redis中,订单服务也无法查询到。
session不能跨不同域名共享的原因
先回顾一下正常的session流程:
在不同域名下会发生什么?
知道了原因后,解决请来就很简单了,只需要在设置cookie的时候,指定domain为父域名fcpmall.com即可。
@Configuration
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainName("fcpmall.com");
return serializer;
}
}
下面的部分由于我水平有限,写得不太好,所以选看即可
为什么spring session可以在不修改应用程序代码的前提下,将getSession方法替换为Redis查询数据的方式?
原理很简单,在我们添加@EnableRedisHttpSession
注解的时候,它会为我们创建一个名为springSessionRepositoryFilter
的bean,这个bean实现了Filter
接口,在过滤器中将原先的HttpSession
替换掉了,采用了装饰者模式。
下面进行初浅的源码分析(源码这一块虽然我现在还很弱,源码也很难读,但我认为这一块还是必要去锻炼的,所以慢慢来吧)
//在EnableRedisHttpSession中导入RedisHttpSessionConfiguration配置类
@Import({RedisHttpSessionConfiguration.class})
@Configuration(
proxyBeanMethods = false
)
public @interface EnableRedisHttpSession{
...
}
@Configuration(
proxyBeanMethods = false
)
//RedisHttpSessionConfiguration继承SpringHttpSessionConfiguration
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
//注入sessionRepository,用来对redis进行增删操作的类
@Bean
public RedisIndexedSessionRepository sessionRepository() {
。。。
}
...
}
//看看SpringHttpSessionConfiguration做了些什么
@Configuration(
proxyBeanMethods = false
)
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
//在容器中注入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;
}
...
}
//这个过滤器中实现了狸猫换太子
@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
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);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
//注意了,传入下一个过滤器的request和response已经被换成了wrappedRequest,wrappedResponse。这里使用了装饰者模式
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
wrappedRequest.commitSession();
}
}
}
到这里知道了,当我们使用spring session的时候,在经过spring session过滤器的时候HttpServletRequest
已经被换成了SessionRepositoryResponseWrapper
,接下来我们就看一下这个类对getSession动了什么手脚。
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();
if (currentSession != null) {
return currentSession;
} else {
//获取session
S requestedSession = this.getRequestedSession();
.....
}
private S getRequestedSession() {
if (!this.requestedSessionCached) {
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
Iterator var2 = sessionIds.iterator();
while(var2.hasNext()) {
String sessionId = (String)var2.next();
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
//从sessionRepository获取session
S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}
}
还记得前面RedisHttpSessionConfiguration
配置的RedisIndexedSessionRepository
吗?被spring session狸猫换太子后,我们后面对HttpSession的操作其实都是由这个类完成的。也就是说对session的增删操作实际上已经换成了对redis的增删操作了。