Cookie和Session
# 背景引入
想必大家都有这样的经验:
- 登录京东,选了 iphone 12 放入购物车后关闭浏览器。再次打开时,发现又要重新登录。
- 登录淘宝,在一个页面逗留了很久,终于决定要买了,却提示你重新登录。
这一切,仿佛是服务器在“监视”着浏览器的行为:
- 咦?浏览器被关了,应该是不买了,我把登录状态也掐了吧。
- 这个页面30分钟没动静了,用户可能已经离开。为了防止路过的人使用该账号,我把它的登录状态掐了吧。
但我们知道现在绝大部分接口都是基于“请求-响应”这种通信模式的,即服务器不会主动搭理客户端,只是被动地响应客户端的请求。况且全中国那么多用户,相比起来,京东淘宝那几台服务器在数量上简直微不足道,不可能监控得过来。既然服务器并不知道客户端发生了什么,那上面的两种情形,是如何做到的呢?
答案就是:会话跟踪。等我们完全了解会话跟踪技术后,再回头解答上面的疑惑。
# 什么是会话
前约临行少留会话,终不克遂,至今为恨。
—— 欧阳修 《与吴正肃公书》
大家可能早就听过这句话:从打开一个浏览器访问某个站点,到关闭这个浏览器的整个过程,称为一次会话。这句话固然精辟,但是很明显不是说给初学者听的。对于不清楚底层发生了什么的人,听完之后可能反而冒出更多疑问:
- 为什么浏览器关闭代表一次会话结束?
- 关闭时发生了什么操作导致会话结束?
- 为什么那个操作执行意味着会话结束?
这三个问题,其实归结起来是一个问题:什么是会话。只要知道了什么是会话,一切迎刃而解。
# 为什么需要会话机制?
会话机制最主要的目的是帮助服务器记住客户端状态(标识用户,跟踪状态)。目前,客户端与服务器的通讯都是通过 HTTP 协议。而 HTTP 是一种无状态协议。Web服务器本身不能识别出哪些请求是同一个浏览器发出的。为了更好的用户体验(比如实现购物车),就有了会话机制。有了它,服务器可以记住并区分客户端,把同一个客户端的操作归类在一起。否则我用我的电脑在京东买了东西,你用你的电脑打开京东却发现购物车里塞满了商品,你乐意吗?所以会话跟踪技术就成了对HTTP无状态协议的一种扩展。
# 如何定义一个会话
基于上面的分析(会话是为了唯一标识一个用户并记录其状态),既然一个会话可以帮助服务器断定一个客户端,那么反向推导得到的结论就是:**当服务器无法断定客户端时,一次会话就结束了!**服务器什么情况下无法断定一个客户端?无非两种情况:
- 服务器不行了(session失效)
- 客户端不行了(cookie失效)
又基于上面分析,可以总结出会话的基本原则:双方共存(cookie与session同在)
# 认识 Cookie
在 Java 的世界里,会话跟踪常用的有两种技术:Cookie 和 Session,并且 Session 底层依赖于 Cookie(其实也可以使用自定义 token,暂不讨论)。当然啦,如果Cookie被禁用,也可以通过 URL 重写等手段实现,但这里不涉及。因为只要你真的明白这篇文章所讲的全部内容,URL 重写理解起来很简单。
我希望大家在看接下来的文字时,要始终提醒自己会话机制的目的是什么:标识用户,跟踪状态。
先来看一下生活中的一个场景:你约人去下馆子。那个馆子远近闻名,每天都要接待很多顾客,座无虚席。于是你打算早一点去店里预定位置。店门口有一台取号机。稍早些到店里的顾客,店员会用取号机打印一张票据给客人,上面记录了一些信息。此时距离约定就餐时间还有段时间,于是你打算去附近西湖逛逛。当你逛完西湖回来时,把票据交给店员,店员一看票据:bravo1988,18:00,69桌。于是把你领到指定位置就餐。
从编程的角度来讲,Cookie其实是一份小数据,是服务器响应给客户端,并且存储在客户端的一份小数据。下次客户端访问服务器时,会自动带上这个Cookie。服务器通过Cookie就可以区分客户端。
# 服务器端如何将Cookie发给浏览器呢?
- 最简单的代码:服务器端 new 一个 cookie,通过 response 的
addCookie
方法响应给浏览器
- 浏览器访问 SendCookieServlet,Servlet 通过 Response 向客户端发送 Cookie
浏览器接收到这些响应头后,会把它们作为Cookie文件存在客户端。当我们第二次请求同一个服务器,浏览器在发送HTTP请求时,会带上这些 Cookie:
# Cookie 的两种类型
- 会话Cookie (Session Cookie)
- 持久性Cookie (Persistent Cookie)
上面代码中,服务器向浏览器响应的Cookie就是会话Cookie。会话Cookie被保存在浏览器的内存中,当浏览器关闭时,内存被释放,内存中的Cookie自然也就烟消云散。
这样太麻烦了,关闭浏览器引发C ookie 消失,下次还要重新登录。能不能向客户端响应持久性Cookie呢?只要设置Cookie的持久化时间即可:coocie.setMaxAge(10 * 60)
,这样就可以看到:
- Set-Cookie 的内容中,多了一个 Expires,它代表过期时间,比如现在时间是12:00,你设置MaxAge=10*60,则过期时间就是12:10(注意,显示的时间不是北京时间,我们在东八区)
小结一下
- 不设置 MaxAge,默认响应会话Cookie(MaxAge<0),存在浏览器内存。Cookie随浏览器关闭而消失
- 设置 MaxAge>0,响应持久性 Cookie,会存在电脑硬盘的特定文件夹下(浏览器自定义的)
- 设置特定Cookie的 MaxAge=0,则会删除已经存在客户端的此 Cookie
至此,其实我们已经可以解释下面的问题:
- 为什么浏览器关闭代表一次会话结束?
- 关闭时发生了什么操作导致会话结束?
- 为什么那个操作执行意味着会话结束?
一般,响应给客户端的Cookie都是会话Cookie(不设置MaxAge),是存在浏览器内存中的。所以关闭浏览器后,内存中Cookie就消失了。Cookie消失,则下次请求服务器时,请求头中不存在代表用户信息的Cookie(唯一标识用户,表示其状态),那么浏览器就无法识别请求的用户。根据我们上面反向推导的结论:当服务器无法断定客户端时,一次会话就结束了!
每家浏览器厂商持久性Cookie存储的位置是不同的
# 认识 Session
有了Cookie,似乎已经能解决问题,为什么还需要Session?原因似乎可以列举很多,有可能是出于安全性和传输效率的考虑。首先,Cookie是存在客户端的,如果保存了敏感信息,会被其他用户看到。其次,如果信息太多,可能影响传输效率。但这些都是我由果推因得到的。总之,Session 会话技术都出来,肯定有它的道理。
相比较Cookie存在客户端,Session 则是服务端的东西。其本质上类似于一个大Map,里面的内容,以键值对的形式存储。这回,我们不再把 name=brava1988;time=6pm;table=69
这样的数据作为 Cookie 放在请求头/响应头里传来传去了,而是只给客户端传一个 JSESSIONID(其实也是一个 Cookie)!此时,真正的数据存在服务器端的 Session中,Cookie 中只是存了Session的 id,即 JSESSIONID。下次访问该网站时,把JSESSIONID带上,即可在服务器端找到对应的Session,也相当于“带去”了用户信息。
- 假设现在服务端有当个 Session,根据 JSESSIONID 获取对应的 Session
protected void doGet(...) {
....
HttpSession session = request.getSession();
session.setAttribute("name", "yubin");
String id = session.getId(); // 获得 Session 的 id
Cookie cookie = new Cookie("JSESSIONID", id);
cookie.setMaxAge(60 * 10);
response.addCookie(cookie);
....
}
2
3
4
5
6
7
8
9
10
另外要注意的是,Session 有个默认最大不活动时间:30分钟(可在配置文件中修改数值)。也就是说,创建 Session并返回 JSESSIONID 给客户端后,如果 30 分钟内你没有再次访问,即使你下次再带着 JSESSIONID 来,服务端也找不到对应 ID 的 Session 了,因为它已经被销毁。此时你必须重新登录。
现在,我请大家重新仔细看一下上面设置JSESSIONID到Cookie的代码。其实,只要你在服务器端创建了 Session,即使不写 addCookie("JSESSIONID", id)
,JSESSIONID 仍会被作为 Cookie 返回:
protected void doGet(...) {
....
HttpSession session = request.getSession();
session.setAttribute("name", "yubin");
String id = session.getId(); // 获得 Session 的 id
System.out.println(id);
....
}
2
3
4
5
6
7
8
- 注意,这次没有 addCookie(),只是简单打印,用于和响应信息作对比
- 结果服务器默认new一个Cookie,将刚才创建的Session的JSESSIONID返回。默认是会话Cookie,浏览器关闭就消失!
# Session 序列化
所谓 Session 序列化,其实是一个默认行为。它存在的意义在于:比如现在有成千上万个用户在线,用户登录信息都在各自的 Session 中。当服务器不得不重启时,为了不让当前保存在服务器的 Session 丢失,服务器会将当前内存中的 Session 序列化到磁盘中,等重启完毕,又重新读取回内存。这些操作浏览器端用户根本感知不到,因为 session 还在,他不用重新登录:
以Tomcat为例,服务器的Session都会被保存在work目录的对应项目下,关闭服务器时,当前内存中的session会被序列化在磁盘中,变成一个叫 SESSIONS.ser 的文件。
# Session 的钝化和活化
自从改用 Session 后,由于 Session 都存在服务器端,当在线用户过多时,会导致 Session 猛增,无形中加大了服务器的内存负担。于是,服务器有个机制:如果一个 Session 长时间无人访问,为了减少内存占用,会被钝化到磁盘上。也就是说,Session 序列化不仅仅是服务器关闭时才发生,当一个 Session 长时间不活动,也是有可能被序列化到磁盘中。当该 Session 再次被访问时,才会被反序列化。这就是 Session 的钝化和活化。
相关配置
可以在Tomcat的conf目录下的context.xml中配置(对所有项目生效):
还有个问题需要解决:Session被序列化了,存在Session中的值怎么办?比如我之前有这么一步操作:
HttpSession session= request.getSession();
session.setAttribute("user", new User("yubin", 26));
2
此时Session中有一个User对象,那么User对象去哪了?答案是,User从内存中消失,无法随Session一起序列化到磁盘。如果希望 Session 中的对象也一起序列化到磁盘,该对象必须实现 Serializable 接口:
public class User implements Serializable {
...
}
2
3
其实也很好理解,有一个大气球(Session),里面有很多小气球(对象),现在大气球要放气,里面的小气球必须也放气。
最后回答开头的问题:之所以服务器能“知道”浏览器关了,是因为下次再来的时候,并没有带来上次给它的 cookie。**你无法证明你还是你。**其实不一定是浏览器关了,也可能是清缓存了。**总之,会话已经结束。**笼统的讲,也可以说浏览器确实给服务器“发消息了”,毕竟HTTP请求头中没有原来的 Cookie,也就相当于告诉它这件事了。