一、传统Session机制及身份认证方案
1、Cookie与服务器的交互
如上图,http是无状态的协议,客户每次读取web页面时,服务器都打开新的会话,而且服务器也不会自动维护客户的上下文信息。比如我们现在要实现一个电商内的购物车功能,要怎么才能知道哪些购物车请求对应的是来自同一个客户的请求呢?session就是一种保存上下文信息的机制,它是针对每一个用户的,变量的值保存在服务器端,通过SessionID来区分不同的客户。session是以cookie或URL重写为基础的,默认使用cookie来实现,系统会创造一个名为JSESSIONID的值输出到cookie。
注意JSESSIONID是存储于浏览器内存中的,并不是写到硬盘上的,如果我们把浏览器的cookie禁止,则web服务器会采用URL重写的方式传递Sessionid,我们就可以在地址栏看到 sessionid=KWJHUG6JJM65之类的字符串。 通常JSESSIONID是不能跨窗口使用的,当你新开了一个浏览器窗口进入相同页面时,系统会赋予你一个新的sessionid,这样我们信息共享的目的就达不到了。
2、服务器端的session的机制
session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。但程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否包含了一个JSESSIONID标识的sessionid,如果已经包含一个session id则说明以前已经为此客户创建过session,服务器就根据sessionid把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。
如果客户请求不包含 session id,则为此客户创建一个session并且生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。对每次http请求,都经历以下步骤处理:服务端首先查找对应的cookie的值(sessionid)。 根据sessionid从服务器端session存储中获取对应id的 session数据,进行返回。 如果找不到sessionid,服务器端就创建session,生成sessionid对应的cookie,写入到响应头中。
3、基于session的身份认证
看下图:
因为http请求是无状态请求,所以在Web领域,几乎所有的身份认证过程,都是这种模式。
二、集群下Session困境及解决方案
如上图,随着分布式架构的流行,单个服务器已经不能满足系统的需要了,通常都会把系统部署在多台服务器上,通过负载均衡把请求分发到其中的一台服务器上,这样很可能同一个用户的请求被分发到不同的服务器上,因为session是保存在服务器上的,那么很有可能第一次请求访问的A服务器,创建了session,但是第二次访问到了B服务器,这时就会出现取不到session的情况。我们知道,Session一般是用来存会话全局的用户信息(不仅仅是登陆方面的问题),用来简化/加速后续的业务请求。要在集群环境下使用,最好的的解决办法就是使用session共享:
1、Session共享方案
传统的session由服务器端生成并存储,当应用进行分布式集群部署的时候,如何保证不同服务器上session信息能够共享呢?两种实现思路:session集中存储(redis,memcached,hbase等)。不同服务器上session数据进行复制,此方案延迟问题比较严重。 我们一般推荐第一种方案,基于session集中存储的实现方案,见下图:
具体过程如下:
新增Filter拦截请求,包装HttpServletRequest(使用HttpServletRequestWrapper) 改写getSession方法,从第三方存储中获取session数据(若没有则创建一个),返回自定义的HttpSession实例在http返回response时,提交session信息到第三方存储中。
2、需要考虑的问题
2.1、需要考虑以下问题:
session数据如何在Redis中存储?
session属性变更何时触发存储?
2.2、实现:
考虑到session中数据类似map的结构,采用redis中hash存储session数据比较合适,如果使用单个value存储session数据,不加锁的情况下,就会存在session覆盖的问题,因此使用hash存储session,每次只保存本次变更session属性的数据,避免了锁处理,性能更好。如果每改一个session的属性就触发存储,在变更较多session属性时会触发多次redis写操作, 对性能也会有影响,我们是在每次请求处理完后,做一次session的写入,并且写入变更过的属性。如果本次没有做session的更改,是不会做redis写入的,仅当没有变更的session超过一个时间阀值(不变更session刷新过期时间的阀值),就会触发session保存,以便session能够延长有效期。
3、代码实战
3.1、新建项目springboot-session-bussiness
主要实现正常的登录拦截功能,为后面项目改造提供参考。
代码参见:springboot-platform------>springboot-session-bussiness模块
代码git地址:https://gitee.com/hankin_chj/springboot-platform.git
3.2、新建springboot-session-bussiness模块
该模块为公共模块,主要用于为其他服务提供统一的session拦截、分装等功能,主要对系统的request和session对象进行重写,其他服务需要用到request的时候就使用封装好的MyRequestWrapper来获取session相关内容。
1)重写HttpSession
public class MySession implements Serializable,HttpSession {
private static final long serialVersionUID = -3923541488767125713L;
private String id;
private Map<String,Object> attrs;
.....
2)request封装代码实现如下:
public class MyRequestWrapper extends HttpServletRequestWrapper {
private volatile boolean committed = false;
private String uuid = UUID.randomUUID().toString();
private MySession session;
private RedisTemplate redisTemplate;
public MyRequestWrapper(HttpServletRequest request,RedisTemplate redisTemplate) {
super(request);
this.redisTemplate = redisTemplate;
}
// 提交session内值到redis
public void commitSession() {
if (committed) {
return;
}
committed = true;
MySession session = this.getSession();
if (session != null && null != session.getAttrs()) {
redisTemplate.opsForHash().putAll(session.getId(),session.getAttrs());
}
}
//创建新session
public MySession createSession() {
String sessionId = CookieBasedSession.getRequestedSessionId(this);//从页面传来的
Map<String,Object> attr ;
if (null != sessionId){
attr = redisTemplate.opsForHash().entries(sessionId);
} else {
System.out.println("create session by rId:"+uuid);
sessionId = UUID.randomUUID().toString();
attr = new HashMap<>();
}
//session成员变量持有
session = new MySession();
session.setId(sessionId);
session.setAttrs(attr);
return session;
}
//或取session
public MySession getSession() {
return this.getSession(true);
}
// 取session
public MySession getSession(boolean create) {
if (null != session){
return session;
}
return this.createSession();
}
//是否已登陆
public boolean isLogin(){
Object user = getSession().getAttribute(SessionFilter.USER_INFO);
return null != user;
}
}
3)包装request对象,提交session到redis,SessionFilter代码实现: