摘自 <<精通 Java Web 开发>>
1 Session的作用
Session 用于维护同一客户端发送的多个请求的状态。
由于 HTTP 协议是无状态协议,从服务器的角度来看,客户端浏览器与服务器间建立起 socket 连接之后,就是一个请求的开始,而服务器发送最后一个数据包到客户端,然后关闭连接。之后就没有客户端与服务器间的连接了,再之后即使是相同客户端过来的请求,也是通过一条新的连接发送的。这样便无法区分当前请求和以前请求是否是同一个客户端发送过来的。
如果从现实生活中来看,就好比是去买东西,每次只能买一件,买了结账就相当于获取了请求。然后再买再结账。。。这样很麻烦。所以好的办法是将东西放入购物车,一起结账。不必每次都去结账。
购物车在这里起到的就是一个状态维护的角色,这样的任务也是Session想达成的。
Session的一些典型应用场景:
- 记录用户
- 支持工作流
实现Session的两种途径:
- Session cookie
- URL重写
不过总体流程都是:
B第一次提出请求 --->服务器收到请求,若客户端没有发送Session Cookie过来,就认为它是第一次请求 ---->通过Session机制为客户端分配一个Session,并生成对应的Session ID --->将生成的ID返还给客户端(通过Cookie机制或重写URL加入Session ID)
相同B的以后请求 --->服务器收到,检查有 Session,查询 Session ID ---> 获取对应的存放在服务器的 Session --->进行状态维护。
而 Session Cookie 或重写的 URL 只负责携带 Session ID,Session 对象由服务器存储。
当设置JSP中page指令的session属性为false时,访问该页面就不会产生Session Cookie了。
<%@ page session="false" %>
并且可以在web.xml中对session cookie的生命期进行设置。
默认的生命期是当本次会话结束(关闭浏览器)
2 安全问题
session的使用中很大可能受到session劫持。
- 复制session,别人就可以通过这个session id访问到你的内容了。
- 让别人通过一个构造的夹杂由session id的url访问,则可以通过相同id来之后查看内容。
- 跨站脚本攻击是使用javascript脚本来读取本地cookie,然后再在自己本地构造一个仿冒的session cookie(或使用url内嵌session id)。(防范就是可以使用httponly, chrome中的这个选项解释地不错:Accessible to script:No (HttpOnly),即启用httponly就是不允许脚本访问该cookie,而只能是url或链接才可以访问。
- 中间人攻击:它是通过一个中间人观察到请求和响应。不过使用HTTPS可以很好防范这样的偷窥。。。不过一个突出问题是当网站非纯HTTPS(即使是从HTTP直接重定向HTTPS),第一次请求的Session也是通过HTTP发送,而非加密的HTTPS。
最佳防范:
- 使用SSL/TLS Session ID:SSL协议定义了它自己的Session id类型。SSL Session ID在SSL握手的时候建立。然后被用于以后的请求,它被用于决定使用哪个key来加密和解密。而且SSL Session ID即不使用Cookie存储,也不使用URL内嵌。(具体参见RFC2246)这样也就避免了使用Cookie或URL内嵌所造成的安全问题。
但之前版本的JAVA EE实现SSL Session十分困难,直到EE 6才提供了一个简单的途径:使用secure标志的Cookie。它就是SSL Session Cookie。不过它要求你的站点必须始终使用的是HTTPS协议。
使用SSL Session的另外一个问题是web容器必须能够支撑SSL交流。如果架构的前端使用的是一个web服务器或者负载均衡服务器,则web容器是无法得知请求的SSL Session ID的。在这样的分布式环境下使用SSL Session ID,则用户请求就还必须总是被路由到web容器(而不是去拦截区分动态静态请求)。
最后,SSL Session ID的生命期可能很长,也可能很短,这是基于服务器和浏览器决定的。所以目前它仍然无法大规模替代HTTP Session ID。
3 在服务器端Session对象中存放数据
首先来看看如何在web.xml中配置Session:
<session-config>
// 略
</session-config>
4 Session的其他用途:
用户登陆:可以在session中加入已登陆的session attribute。
用户登出:session的invalid方法。
5 使用Listener来监听session变化
实现HttpSessionXXXListener接口,然后在web.xml中写listener标签或为该类添加WebListener注解即可。
监听Session创建销毁,session属性改变,id改变等等。(其中改变id是通过request的changesessionid来做的)
获取session列表的方案:
由于没有提供获取全部session的途径,所以只能是当服务器开启后监听session的创建,然后将session放入一个数据结构中保持。以便查询了。。。编程时貌似是只有这样的。
6 利用Session来支持分布式应用
要搞企业级应用,就需要使用分布式。可以让应用得到很好支持。前端可以由很多个不同的负载均衡,也有很多应用服务器,但是数据服务器必须是同步的。分布式的数据的同步问题,还是抽象成多个读者一个写者的读者写者问题吧。
但搞分布式的一个挑战就是:在不同应用服务器上运行的应用组成部分之间的信息交换问题。所以之前出现了许多的解决方案,比如AMQP,JMS,MSMQ等协议,都是解决这个问题的。当然,分布式应用需要解决的问题绝不仅仅只有这一个。
下面先来看看如何在分布式应用中使用Session ID。
6.1 在分布式应用中使用Session ID们 :)
目前的情况是:一个特定的Session只会存在与一个单独的应用服务器内存中,并且作为一个单独的对象存在。
在负载均衡服务器和应用服务器协同工作的场景下,可能同一客户端的两次连续请求会被分发到不同的应用服务器中处理,造成第一个请求被服务器A分配一个Session ID,第二个请求被送达服务器时,携带的是这个服务器上不存在的Session ID,故第二个服务器又重新为第二个请求分配新的Session ID。也就是说每次请求被送到不同服务器上,只要该请求携带的Session ID不是这个服务器分配的,则请求相对于服务器而言就是新的一个客户的请求,那这样的话要Session有毛用。。。
一个解决方案是:sticky session。意思是让负载均衡机制能够识别session id并将对应请求转发给对应的分配session id的应用服务器处理。这样的机制也是比较容易实现的,比如先让负载均衡服务器可以读取session-cookie,这样来判断该cookie对应的应用服务器从而进行转发。或者是负载均衡服务器增加自己的session-cookie到响应中。(不懂)
但这个解决方案存在问题:阻碍使用SSL/HTTPS的使用。因为使用它们后,负载均衡服务器就无法再对请求进行修改或截获了。但是可以让负载均衡服务器来实现HTTPS/SSL通信。
Tomcat环境实现负载均衡的措施是在前端使用一个Apache HTTPD或IIS作为web服务器,将请求进行负载均衡到后端的Tomcat服务器实例上。而Tomcat Connector 提供了Tomcat与web服务器间的通信连接。Connector的mod_jk组件是Apache HTTPD的一个模块,该模块将动态请求转发给后端Tomcat服务器,并提供sticky session支持(即上面说的将对应session的请求发送给对应的服务器,而利用的就是Session cookie中的Session ID们。。。)。
与mod_jk类似,IIS上的connector有一个isapi_redirect组件,也是提供相似功能。
当请求数目进一步提升时,还可以直接在web服务器前端加入一个简单的轮询负载均衡服务器,将请求分配给不同的web服务器(这些web服务器再将负载进一步转发给不同的应用服务器)。
上面提到的这种分层结构可以实现很高的扩展性。
connector使用的是Tomcat中的一个概念:session id jvmroute。这个概念要解决的问题就是将哪个请求转发给哪个对应的Tomcat实例,而利用的正是Session ID。
比如有如下的Session id:abc123(实际的当然比这个长得多,这里仅演示)
在负载均衡环境下,后端有多台Tomcat应用服务器(Tomcat实例),每个Tomcat实例都会拥有一个jvmroute配置(在Tomcat的conf/server.xml文件的<Connector>
元素中配置)。而这个jvmroute的值会被附加到每个session id末尾。
比如此时有三台Tomcat实例,每个分别对应jvmroute值为tcin01,tcin02,tcin03。
不同Tomcat实例生成的session ID就会是如下:
abc123.tcin02 //比如第二台,就是sessionID.jvmroute的形式
这种情况下当请求到达后的话,Connector(HTTPD服务器的mod_jk模块或IIS的isapi_redirect模块)就能够根据请求携带的session id来对应地将请求转发到不同Tomcat实例。
若应用是使用HTTPS的,则需要让web服务器来验证请求并进行加密解密工作。而使用mod_jk或isapi_redirect模块好处是:它们能够访问SSL Session ID并将ID传送给Tomcat。Tomcat也可以替换为GlassFish(其他不变)。
6.2 理解Session复制和失效备缓
sticky session的一个主要问题是:它可以实现高扩展性,但无法实现高可用性。
比如当创建某个session的Tomcat挂掉之后,这个对应的session的用户就相对于又成了新用户了,又需要如重新登陆等,更有可能是用户丢失没有保存的数据。正因为如此,下面提出一种解决办法:session的复制。简单来说就是将每个应用服务器上的session都复制,这样所有服务器上的所有session对所有服务器都可用。
而在应用用打开session复制功能也很简单:在web.xml中加入一句 <distributable />
即可。
这个标签告诉web容器,若是在分布式环境下的话,就进行session复制。(如果某个session发生改变,则这个改变也会重新被复制到每个应用服务器中。
不过设置这个标签只是为了向web容器说明该应用支持session复制。而实际使用时还需要配置web容器,开启容器的session复制机制。
但在开启了session复制的环境下,就必须额外小心地进行session的属性的设置或改变等操作了。(比如session中的某个属性内存放的是一个集合,对这个集合中的元素的改变并不会触发session的重新复制。故要对其中集合进行操作的话,最好是在外部操作集合,然后进行一次setAttribute设置该集合到session中,这样才会触发重新复制的动作)
另外要注意一个监听器HttpSessionActivationListener,这个监听器用于监听Session的序列化(因为复制时需要先序列化再反序列化),当进行序列化动作时就会触发监听器的sessionWillPassive方法。而当该序列再在另外容器中被反序列化时,会触发监听器的sessionDidActivate方法。
最后要指出的是:sticky session id机制和Session复制机制并不互斥。实际上二者经常结合使用以满足session的失效备缓。(session仍然被复制到每个容器,当某容器挂掉后,该容器对应的请求会被转发给另外的容器,因为另外的容器是可用识别这个session的。)
另外可以使用sticky session failover机制来达到高扩展和高可用,不过这个现在不作讨论。一般在web容器文档可用找到如何实现session复制的配置。