第十章 深入理解Session与Cookie
- 10.1 理解Cookie
Cookie属性项
当前Cookie两个版本:Version 0和Version 1,通过设置两种响应头的标识,分别是“Set-Cookie”和“Set-Cookie2”


Cookie如何工作
String getCookie(CookieHandler[] cookies,String key) { if(cookies != null) { for(Cookie cookie:cookies) { return cookie.getValue(); } } return null; } @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException,ServletException{ Cookie[] cookies = request.getCookies(); String userName = getCookie(cookies, "userName"); String userAge = getCookie(cookies, "userAge"); if(userName == null) { response.addCookie(new Cookie("userName", "junshan")); } if(userAge == null) { response.addCookie(new Cookie("userAge", "28")); } response.getHeaders("Set-Cookie"); }

注意一下几点:
1、创建的Cookie的NAME不能和Set-Cookie或者Set-Cookie2的属性一样,否则会抛出IllegalArgumentException异常
2、创建Cookie的NAME和VALUE的值不能设置成非ASSIC字符,用的话可以用URLEncoder
3、当NAME和VALUE的值出现一些TOKEN字符时,构建返回头会将该Cookie的Version自动设置为1
4、当该Cookie的属性中出现Version为1的属性时,构建HTTP响应头同样会将Version设置为1
public void se(){ int size = headers.size(); for (int i = 0; i < size; i++) { outputBuffer.sendHeader(headers.getName(i), headers.getValue(i)); } }
这段代码显示在构建HTTP返回字节流时将Header中所有的项顺序地写出,而没有进行任何修改。所以可以想象浏览器在接受HTTP协议返回的数据时分别解析每一个Header项。
- 10.2 理解Session
同一个客户端每次和服务端交互时,不需要每次都传回所有的Cookie值,而是只要传回一个ID,这个ID是客户端第一次访问服务器的时候生成的鳄,而且每个客户端是唯一的。这样每个客户端就有一个唯一的ID。客户端只要回传这个ID就行,这个ID通常是NAME为JSESIONID的一个Cookie。
Session与Cookie
Session正常工作的三种方式:
1、基于URL Path Parameter,默认支持
2、基于Cookie,如果没有修改Context容器的cookies标识,默认也是支持的
3、基于SSL,默认不支持,只有connector.getAttribute("SSLEnabled")为TRUE时才支持
Session如何工作
根据Session ID服务端创建HttpSession对象,第一次触发通过request.getSession()方法。如果当前的Session ID还没有对应得HttpSession对象,那就创建一个新的,并将这个对象加到org.apache.cataline.Manager的session容器中保存。Manager类将管理所有Session的生命周期,Session过期将被收回,服务器关闭,Session将被序列化到磁盘。


从时序图看,从Request中获取的Session对象保存在org.apache.catalina.Manager类中,它实现的类是org.apache.catalina.session.StandardManager,通过requestedSessionId从StandardManager的sessions集合中取出StandardSession对象。由于一个requestedSessionId对应一个访问的客户端,所以一个客户端也对应一个StandardSession对象,这个对象保存在Session中。

StandardManager类负责Servlet容器中所有的StandardSession对象的生命周期管理。当Servlet容器重启或关闭时StandardManager负责持久化没有过期的StandardSession对象,它会将所有的StandardSession独享持久化到一个以"SESSIONS.ser"的文件中。到Servlet容器重启时,也就是StandardManager初始化时,会将读取这个文件解析出所有Session对象,重新保存在StandardManager的session集合中。

当Servlet容器关闭时StandardManager类会调用unload方法将sessions集合中StandardSession对象写到SESSIONS.ser文件中,然后启动时再按上图重新恢复,注意要持久化保存Servlet容器中的Session对象,必须调用Servlet容器中的stop和start命令,而不能直接结束(Kill)Servket容器的进程,因为直接结束进程,Servlet容器没有机会调用unload方法来持久化这些Session对象。

除了后台进程检查Session是否失效外,当调用request.getSession()时也会检查该Session是否过期。request.getSession()方法调用的StandardSession永远都会存在,即使与这个客户端关联的Session对象已经过期。
- 10.3 Cookie安全问题
Cookie通过把所有要保存的数据通过HTTP协议的头部从客户端传递到服务端,又从服务端再传到客户端,所有的数据都保存在客户端的浏览器里
Session是将数据保存在服务端,只是通过Cookie传递一个Session ID
- 10.4 分布式Session框架
存在哪些问题
1、客户端存储限制,Cookie个数限制在50个
2、Cookie管理的混乱,每个应用都有自己的Cookie
3、安全担忧
可以解决哪些问题
1、Session配置的统一管理
2、Cookie使用的监控和统一规范管理
3、Session存储的多元化
4、Session配置的动态修改
5、Session加密Key的定期修改
6、充分的容灾机制,保持框架的使用稳定性
7、Session各种存储的监控和报警支持
8、Session框架的可扩展性,兼容更多的session机制如wapSession
9、跨域名Session与Cookie共享
总体实现思路

session配置
<sesion> <key>sessionID</key> <ookiekey>sessionID</ookiekey> <lifeCycle>9000</lifeCycle> <base64>true</base64> </session>
cookie配置
<cookie> <key>lifeCycle</key> <lifeCycle></lifeCycle> <type>1</type> <path>/wp</path> <domain>xulingbo.net</domain> <decrypt>false</decrypt> <httpOnly>false</httpOnly> </cookie>
通过统一订阅服务器推送配置可以有效集中管理资源省去每个应用都来配置Cookie,简化Cookie的管理。而共享这些Session必须将它们存储在分布式缓存中,可以随时写入和读取。
分布式Session的处理框架,必然会重新实现HttpSession的操作接口,使得应用操作Session对象都是我们实现的InnerHttpSession对象,这个操作必须在进入应用之前完成,所以可以配置一个filter拦截用户的请求。

我们可以在应用的web.xml中配置一个SessionFilter,用于在请求到达MVC框架之前封装HttpServletRequest和HttpServletResponse对象,并创建我们自己的InnerHttpSession对象,把它 设置到request和response对象中。这样通过request.getHttpSession()返回的就是我们创建的InnerHttpSession对象了,我们可以拦截response的addCookies设置的Cookie。
在时序图中,应用创建的所有Session对象都会保存在InnerHttpSession的所有内容再更新到分布式缓存中,以便于这个用户通过其他服务器再次访问这个应用系统。另外,为了保证一些应用对Session稳定性的特殊要求可以将一些非常关键的Session再存储到Cookie中。

从时序图看出,实现Session同步,需要另外一个跳转应用,这个应用可以被一个或多个域名同时访问,主要功能是从一个域名下取得sessionID,然后将这个sessionID同步到另外一个域名下。这个sessionID其实就是一个Cookie,相当于我们经常遇到的JSESSIONID,所以要实现两个域名下的Session同步,必须要将同一个sessionID作为Cookie写到两个域名下。
- 10.5 Cookie压缩
Cookie是在HTTP的头部,所以通常的gzip和deflate针对HTTP Body的压缩不能压缩Cookie,如果Cookie量非常大,可以考虑将Cookie也做压缩,压缩方式是将Cookie的多个k/v对看成普通的文本,做文本压缩。压缩算法同样可以使用gzip和deflate算法,但是需要注意,根据Cookie规范,Cookie中不能包含控制符,仅仅只能包含ASCII码为(34~126)的可见字符。所以要将压缩后的结果再进行转码,可以进行Base32或Base64编码。
package cn.javaweb.tencharacter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; /** * @ClassName: CompressCookie * @Description: TODO(Cooie 压缩) * @author rocky * @date 2016年3月28日 下午3:09:35 */ public class CompressCookie { public CompressCookie() { } //压缩 private void compressCookie(Cookie c, HttpServletResponse res) { try { ByteArrayOutputStream bos = null; bos = new ByteArrayOutputStream(); DeflaterOutputStream dos = new DeflaterOutputStream(bos);//Creates a new output stream with a default compressor and buffer size. dos.write(c.getValue().getBytes()); dos.close(); System.out.println("beform compress length:" + c.getValue().getBytes().length); String compress = new sun.misc.BASE64Encoder().encode(bos.toByteArray()); res.addCookie(new Cookie("compress", compress)); System.out.println("after compress length:" + compress.getBytes().length); } catch (Exception e) { e.printStackTrace(); } } //解压 private void unCompressCookie(Cookie c) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] compress = new sun.misc.BASE64Decoder().decodeBuffer(new String(c.getValue().getBytes())); ByteArrayInputStream bis = new ByteArrayInputStream(compress); InflaterInputStream inflater = new InflaterInputStream(bis); byte[] b = new byte[1024]; int count; while ((count = inflater.read(b)) >= 0) { out.write(b, 0, count); } inflater.close(); System.out.println(out.toByteArray()); } catch (IOException e) { e.printStackTrace(); } } }
- 10.6 表单重复提交问题
要能防止表单重复提交,就要标识用户的每次访问请求,使得每次访问服务端来说都是唯一确定的。为了标识用户的每次访问请求,可以在用户请求一个表单域时增加一个隐藏表单项,这个表单项的值每次都是唯一的token。
<form id="form" mothod="post"> <input type=hidden name="crsf_token" value="xxxx" /> </form>
当用户在请求时生成这个唯一的token时,同时将这个token保存在用户的Session中,等用户提交请求时检查这个token和当前的Session中保存的token是否一致。如果一致,则说明没有重复提交。

上图是用户发起对表单页面的请求过程,生成唯一的token需要一个算法,最简单的是可以根据一个种子作为key生成一个随机数,并保存在Session中,等下次用户提交表单时做验证。


浙公网安备 33010602011771号