深入理解Cookie与Session
Session与Cookie的作用都是为了保持访问用户与后端服务器的交互状态。它们各有优点,也有各自的缺点。比如使用Cookie传递信息时,随着Cookie个数的增多和访问量的增加,它占用的网络带宽也很大,所有有大访问量时希望用Session,但是Session的致命缺点是不容易在多台服务器之间共享,这也限制了Sesssion的使用
理解Cookie
当一个用户通过Http访问一个服务器时,这个服务器会将一些Key/Value键值对返回给客户端浏览器,并给这些数据加上一些限制条件,在条件符合时这个用户下次访问这个服务器时,数据又被完整地带回服务器
当初W3C在设计Cookie时实际上考虑的是为了记录用户在一段时间内访问Web应用的因为路径。有于HTTP是一种无状态协议,当用户的一次访问请求结束后,后端服务器就无法直到下一次来访问的还是不是上次访问的用户。Cookie的作用正是如此,由于是同一个客户端发出的请求,每次发出的请求都会带着第一次访问时服务端设置的信息,这样服务端就可以根据Cookie值来划分访问的用户了
Cookie的创建
// 通过request获得Cookie,这里获取的是所有的Cookie数组,通过遍历数组找到需要的值
Cookie[] cookies = request.getCookies();
// 通过response设置Cookie,可以调用多次addCoookie方法添加多个Cookie
response.addCookie(new Cookie("userName", "XXX"))
所创建的Cookie的Name和Value的值不能设置为非ASCII字符,如果要使用中文,可以通过URLEncoder
将其编码,否则会抛出IllegalArgumentException
异常
当我们通过response.addCookie
创建多个Cookie时,这些Cookie最终是在一个请求Header项中的
默认没有设置Cookie的生命周期,则在浏览器关闭后Cookie就失效了,可以通过setMaxAge(秒)
设置失效期
当我们请求某个URL路径时,浏览器会根据这个URL路径将符合条件的Cookie放在Request请求头回传给服务端,服务端通过request.getCookies()
来获取所有Cookie
使用Cookie的限制
Cookie是HTTP头中的一个字段,虽然HTTP对本身对这个字段没有多少限制,但是Cookie最终还是存储在浏览器里,所以不同的浏览器对Cookie的存储都有一些限制,一般对于Cookie数量限制在500个/每个域名下,Cookie的总大小在4000字节左右
理解Session
Cookie可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问必须回传这些Cookie,如果Cookie很多,则无形中增加了客户端和服务端的数据传输量,而Session的出现正是为了解决这个问题
同一个客户端每次和服务端交互时,不需要每次都回传所有的Cookie值,而是只要传回一个ID,这个ID时客户端第一次访问服务器时生成的,而且每个客户端都是唯一的,这样每个可负担就有了一个唯一的ID,客户端只要传回这个ID就行了,这个ID通常是Name为JSESSIONID
的一个Cookie
Session与Cookie
Session是基于Cookie工作的,实际上有三种方式可以让Session正常工作:
- 基于URL Path Parameter,默认支持
- 基于Cookie,如果没有修改Context容器的Cookie标识,则默认也是支持的
- 基于SSL,默认不支持,只有
connector.getAttributes("SSLEnabled")
为true时才支持
当浏览器不支持Cookie功能时,浏览器会将用户的SessionCookieName
重写到用户请求的URL参数中,它的传递格式如/path/Servlet;name=value;name2=value2?param3=Value3
,其中Servlet;
后面跟着的K-V(name=value;name2=value2
)就是要传递的Path Parameters,服务端会从这个Path Parameters中拿到用户配置的SessionCookieName
,这个SesssionCookieName
如果在web.xml中配置session-config配置项,其cookie-config下的name属性就是这个SessionCookieName
的值;如果没有配置session-config配置项默认的就是我们熟悉的JSESSIONID
如果客户端也支持Cookie,则Tomcat仍然会解析Cookie中的Session ID,并会覆盖URL中的Session ID
如果是第三种情况,则会根据javax.servlet.request.ssl_session
属性值设置Session ID
Session如何工作
有了Session ID服务端就可以创建HttpSession
对象了,第一次触发是通过request.getSession()
方法,如果当前的Session ID还没有对应的HttpSession
对象,那么就创建一个新的,并将这个对象加到org.apache.catalina.Manager
的Session容器中保存。Manager类将管理所有的Session生命周期,Session过期将被回收;服务器关闭没过期的Session将会持久化到硬盘。只要这个HttpSession
对象存在,用户就可以根据Session ID来获取这个对象了,也就做到了对状态的保持
在Tomcat中,从requst.getSession()
方法中获取的HttpSession
对象实际上StandardSession
对象的门面对象StandardSessionFacade
,也是采用的门面模式。实际上管理Session的org.apache.catalina.Manager
类的实现类是org.apache.catalina.session.StandardManager
,通过Session ID从StandardManager
的session集合中取出StandardSession
对象。一个Session ID对应一个访问的客户端,所以一个客户端也就对应一个StandardSession
对象,这个对象才是真正保存我们创建的Session值
Session在服务器端先保存在内存中,当容器关闭时(正常关闭)会持久化到硬盘,具体如下:
当Servlet容器重启或关闭时,StandardManager
类会调用upload方法将session集合中没有过期的StandardSesssion
对象持久化,它会将所有的StandardSession
对象持久化到一个以SESSIONS.ser
为文件名的文件中。到Servlet容器重启时,也就是StandardManager
初始化时,它将重新读取这个文件,解析出所有的Session对象,重新保存在StandardManager
的Session集合中
值得注意的要持久化Session对象,必须调用Servlet容器的stop命令关闭容器(正常关闭),而不能直接kill掉Servlet容器的进程。因为直接结束进程,Servlet容器就没有机会调用upload方法来持久化这些Session对象
Session对象在Servlet容器中并不是永远存在的,否则内存很容易被耗尽,所以必须给每个Session一个失效期,超过这个时间则Session对象将被清除。Tomcat中Session的有效期是30分钟,在代码中使用Session对象的setMaxInactiveInterval(秒)
设置失效时间。检查每个Session是否失效是在Tomcat的一个后台线程中完成的,除了这个后台线程会检查外,当调用requeest.getSession()
时也会检查该Session是否过期
值得注意的是,request.getSession
方法调用永远会返回一个Session对象(StandardSessoin),即使与这个客户端关联的Session对象已经过期。如果过期,则又会重新创建一个全新的Session,但是以前那个设置的Session值将会丢失。所以如果使用session.getAttribute
取不到前面设置的Session值,很可能是Session过期了
如果不想让Session过期可以设置setMaxInactiveInterval(-1)
,但是需要估下网站的访问量和设置的Session大小,防止Servlet容器内存被撑爆。如果不想自动创建Session对象,也可以通过request.getSession(boolean create)
方法来判断该客户端关联的Session对象是否存在,true:Session失效后会自动创建新对象,实际上和无参的方法一致;false:如果当前Session不存在不会自动创建新对象
Cookie的安全性
Cookie通过把所有要保存的数据通过HTTP的头部从服务端传递到客户端,又从客户端回传给服务端,所有的数据保存在客户端的浏览器里,所以这些Cookie数据可以在直接访问到
相比较而言Session的安全性要高很多,因为Session是将数据保存在服务端,只是通过Cookie传递一个Session ID而已,所以Session更适合存储用户隐私和重要的数据
Cookie压缩
Cookie在HTTP头部。所以通常的gzip和deflate针对请求body的压缩不能压缩Cookie,如果Cookie的量很大,则需要手动对Cookie压缩,压缩方式是将Cookie的k/v当作普通文本,做文本压缩。压缩算法同样可以使用gzip和deflate算法。值得注意的是,根据Cookie规范,在Cookie中不能包含控制字符,仅能包含ASCII码为34~126的可见字符。所以要将压缩后的结果再进行转码,可以进行Base32和Base64编码
使用deflater压缩,压缩后使用Base64编码
// deflater压缩
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DeflaterOutputStream dos = new DeflaterOuterOutputStream(bos);
dos.write("数据".getBytes());
dos.close();
// Base64编码
String compress = new sun.misc.BASE64Encoder().encode(bos.toByteArray());
response.addCookie(new Cookie("compress", compress));
先使用Base64解码,再使用Inflater解码
// Base64解码
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] compress = new sun.misc.BASE64Decoder().decodeBuffer("数据");
// inflater解码
ByteArrayInputStream bis = new ByteArrayInputStream(compress);
InfalterInputStream inflater = new InflaterInputStream(bis);
byte[] b= new byte[1024];
int count = 0;
while ((count = inflater.read(b)) >= 0) {
out.write(b, 0, count);
}
inflater.close();
System.out.println(new String(out.toByteArray());
解决表单重复提交问题
要防止表单重复提交就需要标识用户每一次访问请求,使得每一次访问对服务端来说都是唯一确定的。为了标识用户每次访问请求,可以在表单域中添加一个隐藏域,这个隐藏域的值每次都是唯一的,当打开表单时通过后台生成一个唯一的值,并设置到表单的隐藏域中,同时保存到Session中;提交表单时就会将隐藏域值回传与Session中的值对比,如果一致则说明没有重复提交可以后续处理,并删除该Session值;如果两个值比对不上说明这次提交不合法
多终端登录
目前很多网站都会有通过移动端扫码登陆的情况,可以通过如下方式实现:这里移动端设备必须是已经登录的状态,因为这样才知道到底是谁要登录的信息,同时扫码的二维码也带着一个特殊标识,标识是这个客户通过手机登录了,当手机端扫码成功后,会在服务端设置这个二维码对应的标识为登录成功,这是PC客户端会通过轮询请求发送服务端(可以通过定时器),来验证标识位是否已经设置来判断能否登录