深入理解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客户端会通过轮询请求发送服务端(可以通过定时器),来验证标识位是否已经设置来判断能否登录

posted @ 2021-01-31 14:05  OverZeal  阅读(119)  评论(0编辑  收藏  举报