javaEE 深入理解 Session 与 Cookie

来源:互联网 发布:广州红松网络谁去过 编辑:程序博客网 时间:2024/05/07 05:18

        SessionCookie不管是对 Java Web的初学者还是熟练使用者来说都是一个令人头疼的问题。在初入职场时恐怕很多程序员在面试时都被问过这个问题。其实这个问题回答起来既简单又复杂,简单是因为它们本身只是HTTP 中的一个配置项,在 Servlet规范中也只是对应到一个类而已;说它复杂原因在于当我们的系统大到需要用到很多Cookie时,我们不得不考虑 HTTP Cookie数量和大小的限制,那么如何才能解决这个瓶颈呢?Session也会有同样的问题,当我们的一个应用系统有几百台服务器时,如何解决Session在多台服务器之间共享的问题?它们还有一些安全问题,如Cookie被盗、Cookie伪造等问题应如何避免。本章将详细解答这些问题,同时也将分享淘宝在解决这些问题时总结的一些经验。

        SessionCookie的作用都是为了保持访问用户与后端服务器的交互状态。它们有各自的优点,也有各自的缺陷,然而具有讽刺意味的是它们的优点和它们的使用场景又是矛盾的。例如,使用Cookie来传递信息时,随着 Cookie 个数的增多和访问量的增加,它占用的网络带宽也很大,试想假如Cookie占用 200个字节,如果一天的 PV 有几亿,那么它要占用多少带宽?所以有大访问量时希望用Session,但是Session的致命弱点是不容易在多台服务器之间共享,这也限制了Session的使用。 


理解 Cookie 

        Cookie的作用我想大家都知道,通俗地说就是当一个用户通过HTTP 访问一个服务器时,这个服务器会将一些 Key/Value 键值对返回给客户端浏览器,并给这些数据加上一些限制条件,在条件符合时这个用户下次访问这个服务器时,数据又被完整地带回给服务器。

    这个作用就像你去超市购物时,第一次给你办张购物卡,在这个购物卡里存放了一些你的个人信息,下次你再来这个连锁超市时,超市会识别你的购物卡,下次直接购物就好了。

    当初 W3C在设计 Cookie时实际上考虑的是为了记录用户在一段时间内访问Web 应用的行为路径。由于 HTTP是一种无状态协议,当用户的一次访问请求结束后,后端服务器就无法知道下一次来访问的还是不是上次访问的用户。在设计应用程序时,我们很容易想到两次访问是同一人访问与不同的两个人访问对程序设计和性能来说有很大的不同。例如,在一个很短的时间内,如果与用户相关的数据被频繁访问,可以针对这个数据做缓存,这样可以大大提高数据的访问性能。Cookie的作用正是如此,由于是同一个客户端发出的请求,每次发出的请求都会带有第一次访问时服务端设置的信息,这样服务端就可以根据Cookie值来划分访问的用户了。 


Cookie 属性项 

    当前 Cookie有两个版本:Version 0Version 1,它们有两种设置响应头的标识,分别是“Set-Cookie”和“Set-Cookie2”。这两个版本的属性项有些不同,表10-1 和表 10-2是对这两个版本的属性介绍。 

10-1 Version 0属性项介绍 


10-2 Version 1属性项介绍 



    在以上两个版本的 Cookie 中设置的 Header头的标识符是不同的,我们常用的是Set-Cookie:userName=junshan; Domain=xulingbo.net”,这是Version 0的形式。针对 Set-Cookie2是这样设置的:Set-Cookie2:userName=junshan; Domain=xulingbo.net;Max-Age=1000。但是在Java WebServlet规范中并不支持 Set-Cookie2响应头,在实际应用中 Set-Cookie2 的一些属性项却可以设置在 Set-Cookie中,如这样设置:Set-Cookie:userName=junshan; Version=1;Domain=xulingbo.net;Max-Age=1000。 


Cookie 如何工作 

    当我们用如下方式创建 Cookie 时: 

String getCookie(Cookie[] cookies, String key) {if (cookies != null) {for (Cookie cookie : cookies) {             if (cookie.getName().equals(key))              {                 return cookie.getValue();             }} }return null;}@Overridepublic 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");}

        Cookie是如何加到 HTTPHeader中的呢?当我们用 Servlet 3.0 规范来创建一个Cookie对象时,该Cookie既支持Version 0又支持Version 1,如果你设置了Version 1中的配置项,即使你没有设置版本号,Tomcat在最后构建 HTTP响应头时也会自动将 Version的版本设置为1。下面看一下Tomcat是如何调用 addCookie方法的,图 10-1Tomcat创建 Set-Cookie响应头的时序图。 



    从图 10-1中可以看出,真正构建 Cookie 是在 org.apache.catalina.connector.Response类中完成的,调用generateCookieString方法将 Cookie对象构造成一个字符串,构造的字符串的格式如userName=junshan;Version=1; Domain=xulingbo.net; MaxAge=1000。然后将这个字符串命名为Set-Cookie添加到 MimeHeaders中。

    在这里有以下几点需要注意。

  • ◎  所创建CookieNAME不能和 Set-Cookie或者 Set-Cookie2的属性项值一样,

    如果一样会抛出 IllegalArgumentException异常。

  • ◎  所创建CookieNAMEVALUE的值不能设置成非 ASSIC 字符,如果要使用中

    文,可以通过 URLEncoder将其编码,否则会抛出 IllegalArgumentException异常。

  • ◎  NAME VALUE的值出现一些 TOKEN字符(如“\”、“,”等)时,构建返

    回头会将该 CookieVersion自动设置为 1

  • ◎  当在该Cookie的属性项中出现 Version1的属性项时,构建 HTTP 响应头同样

    会将 Version设置为 1

    不知道你有没有注意到一个问题,就是当我们通过 response.addCookie 创建多个Cookie 时,这些 Cookie 最终是在一个 Header 项中的还是以独立的 Header存在的,通俗地说也就是我们每次创建 Cookie 时是否都是创建一个以 NAME 为 Set-Cookie MimeHeaders?答案是肯定的。从上面的时序图中可以看出每次调用 addCookie 时,最终都会创建一个 Header,但是我们还不知道最终在请求返回时构造的 HTTP 响应头是否将相同 Header 标识的 Set-Cookie 值进行合并。

    我们找到 Tomcat 最终构造 Http 响应头的代码,这段代码位于 org.apache.coyote.http11.Http11Processor 类的 prepareResponse 方法中,如下所示: 

int size = headers.size();for (int i = 0; i < size; i++) {   outputBuffer.sendHeader(headers.getName(i), headers.getValue(i));}

    这段代码清楚地表示,在构建HTTP 返回字节流时是将Header 中所有的项顺序地写出,而没有进行任何修改。所以可以想象浏览器在接收HTTP 返回的数据时是分别解析每一个Header 项的。 

    另外,目前很多工具都可以观察甚至可以修改浏览器中的Cookie数据。例如,在 Firefox中可以通过HttpFox插件来查看返回的 Cookie 数据,如图 10-2所示。 



    在 Cookie项中可以详细查看 Cookie 属性项,如图 10-3所示。 



    前面主要介绍了在服务端如何创建 Cookie,下面看一下如何从客户端获取Cookie

    当我们请求某个 URL路径时,浏览器会根据这个 URL 路径将符合条件的Cookie放在 Request请求头中传回给服务端,服务端通过request.getCookies()来取得所有Cookie。 


使用 Cookie的限制 

        CookieHTTP头中的一个字段,虽然 HTTP 本身对这个字段并没有多少限制,但是Cookie最终还是存储在浏览器里,所以不同的浏览器对Cookie的存储都有一些限制,表10-3是一些通常的浏览器对 Cookie 的大小和数量的限制。

10-3浏览器对 Cookie 的大小和数量的限制 




理解 Session 

    前面已经介绍了 Cookie可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问都必须传回这些Cookie,如果Cookie很多,则无形地增加了客户端与服务端的数据传输量,而Session的出现正是为了解决这个问题。

    同一个客户端每次和服务端交互时,不需要每次都传回所有的Cookie值,而是只要传回一个 ID,这个ID 是客户端第一次访问服务器时生成的,而且每个客户端是唯一的。这样每个客户端就有了一个唯一的ID,客户端只要传回这个ID 就行了,这个 ID通常是NANEJSESIONID的一个 Cookie。 

Session Cookie

    下面详细讲一下 Session是如何基于 Cookie来工作的。实际上有以下三种方式可以让Session正常工作。

  • ◎  基于URL Path Parameter,默认支持。

  • ◎  基于Cookie,如果没有修改Context容器的 Cookies标识,则默认也是支持的。

  • ◎  基于SSL,默认不支持,只有connector.getAttribute("SSLEnabled")TRUE 时才支持。

    在第一种情况下,当浏览器不支持 Cookie 功能时,浏览器会将用户的 SessionCookieName重写到用户请求的 URL 参数中,它的传递格式如/path/Servlet;name=value;name2=value2?Name3=value3,其中“Servlet;”后面的K-V就是要传递的Path Parameters,服务器会从这个Path Parameters中拿到用户配置的 SessionCookieName。关于这个SessionCookieName,如果在web.xml 中配置 session-config配置项,其 cookie-config下的 name属性就是这个SessionCookieName的值。如果没有配置 session-config配置项,默认的 SessionCookieName就是大家熟悉的“JSESSIONID”。需要说明的一点是,与Session 关联的 Cookie与其他Cookie没有什么不同。接着 Request根据这个 SessionCookieNameParameters中拿到Session ID并设置到 request.setRequestedSessionId中。

    请注意,如果客户端也支持 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. Managersessions容器中保存。Manager类将管理所有 Session的生命周期,Session过期将被回收,服务器关闭,Session将被序列化到磁盘等。只要这个HttpSession对象存在,用户就可以根据 Session ID 来获取这个对象,也就做到了对状态的保持。

    与 Session相关的类图如图 10-4所示。 



    从图 10-4中可以看出,从 request.getSession中获取的 HttpSession对象实际上是StandardSession对象的门面对象,这与前面的 RequestServlet是一样的原理。图 10-5 Session 工作的时序图。 




    从时序图中可以看出,从 Request 中获取的 Session对象保存在 org.apache.catalina.Manager类中,它的实现类是 org.apache.catalina.session.StandardManager,通过requestedSessionIdStandardManagersessions集合中取出 StandardSession对象。由于一个 requestedSessionId对应一个访问的客户端,所以一个客户端也就对应一个StandardSession对象,这个对象正是保存我们创建的 Session值的。下面我们看一下StandardManager这个类是如何管理 StandardSession的生命周期的。

    图 10-6StandardManagerStandardSession的类关系图。 



        StandardManager类负责 Servlet容器中所有的 StandardSession对象的生命周期管理。当 Servlet 容器重启或关闭时,StandardManager负责持久化没有过期的 StandardSession对象,它会将所有的 StandardSession对象持久化到一个以“SESSIONS.ser”为文件名的文件中。到Servlet容器重启时,也就是 StandardManager初始化时,它会重新读取这个文件,解析出所有Session对象,重新保存在 StandardManagersessions集合中。Session的恢复状态图如图 10-7 所示。 



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

    另外,在 StandardManagersessions集合中的 StandardSession对象并不是永远保存的,否则 Servlet 容器的内存将很容易被消耗尽,所以必须给每个 Session 对象定义一个有效时间,超过这个时间则 Session 对象将被清除。在 Tomcat中这个有效时间是 60s(maxInactiveInterval属性控制),超过 60sSession将会过期。检查每个 Session是否失效是在 Tomcat的一个后台线程中完成的(backgroundProcess()方法中)。过期Session 的状态图如图10-8 所示。 



    除了后台进程检查 Session 是否失效外,当调用 request.getSession()时也会检查该Session是否过期。值得注意的是,request.getSession()方法调用的StandardSession永远都会存在,即使与这个客户端关联的Session对象已经过期。如果过期,则又会重新创建一个全新的StandardSession对象,但是以前设置的 Session 值将会丢失。如果你取到了 Session对象,但是通过session.getAttribute取不到前面设置的 Session 值,请不要奇怪,因为很可能 它 已 经 失 效 了 , 请 检 查 一 下<Manager pathname="" maxInactiveInterval="60" />maxInactiveInterval配置项的值,如果不想让 Session 过期则可以设置为-1。但是你要仔细评估一下,网站的访问量和设置的Session的大小,防止将你的 Servlet 容器内存撑爆。如果不想自动创建 Session 对象,也可以通过 request.getSession(boolean create)方法来判断与该客户端关联的Session对象是否存在。 


Cookie 安全问题 

    虽然 CookieSession都可以跟踪客户端的访问记录,但是它们的工作方式显然是不同的,Cookie通过把所有要保存的数据通过 HTTP 的头部从客户端传递到服务端,又从服务端再传回到客户端,所有的数据都存储在客户端的浏览器里,所以这些Cookie 数据可以被访问到,就像我们前面通过 Firefox的插件 HttpFox可以看到所有的 Cookie值。不仅可以查看 Cookie,甚至可以通过Firecookie 插件添加、修改 Cookie,所以Cookie 的安全性受到了很大的挑战。

    相比较而言 Session的安全性要高很多,因为 Session 是将数据保存在服务端,只是通过 Cookie 传递一个 SessionID而已,所以 Session更适合存储用户隐私和重要的数据。 


分布式 Session 框架 

    从前面的分析可知,SessionCookie各自有优点和缺点。在大型互联网系统中,单独使用CookieSession都是不可行的,原因很简单。因为如果使用Cookie,则可以很好地解决应用的分布式部署问题,大型互联网应用系统的一个应用有上百台机器,而且有很多不同的应用系统协同工作,由于Cookie是将值存储在客户端的浏览器里,用户每次访问都会将最新的值带回给处理该请求的服务器,所以也就解决了同一个用户的请求可能不在同一台服务器处理而导致的Cookie不一致的问题。 


存在哪些问题 

    这种“谁家的孩子谁抱走”的处理方式的确是大型互联网的一个比较简单但的确可以解决问题的处理方式,但是这种处理方式也会带来了很多其他问题,如下所述。

  • ◎  客户端Cookie存储限制。随着应用系统的增多,Cookie数量也快速增加,但浏览器对于用户Cookie的存储是有限制的。例如,对 IE7 之前的IE 浏览器,Cookie个数的限制是20 个;而对后续的版本,包括 Firefox等,Cookie个数的限制都是50个,总大小不超过 4KB,超过限制就会出现丢弃Cookie的现象,这会严重影响应用系统的正常使用。

  • ◎  Cookie管理的混乱。在大型互联网应用系统中,如果每个应用系统都自己管理每个应用使用的Cookie,则会导致混乱,由于通常应用系统都在同一个域名下,Cookie又有上面一条提到的限制,所以没有统一管理很容易出现Cookie超出限制的情况。 

安全令人担忧。虽然可以通过设置 HttpOnly 属性防止一些私密 Cookie被客户端访问,但是仍然不能保证 Cookie 无法被篡改。为了保证 Cookie的私密性通常会对 Cookie 进行加密,但是维护这个加密 Key也是一件麻烦的事情,无法保证定期更新加密Key 也是带来安全性问题的一个重要因素。

    当我们对以上问题不能再容忍下去时,就不得不想其他办法处理了。 


可以解决哪些问题

    既然 Cookie有以上问题,Session也有它的好处,那么为何不结合使用SessionCookie呢?下面是分布式Session 框架可以解决的问题。

  • ◎  Session配置的统一管理。

  • ◎  Cookie使用的监控和统一规范管理。

  • ◎  Session存储的多元化。

  • ◎  Session配置的动态修改。

  • ◎  Session加密 key的定期修改。

  • ◎  充分的容灾机制,保持框架的使用稳定性。

  • ◎  Session各种存储的监控和报警支持。

  • ◎  Session框架的可扩展性,兼容更多的 Session 机制如 wapSession

  • ◎  跨域名SessionCookie如何共享的问题。现在同一个网站可能存在多个域名,如何将SessionCookie在不同的域名之间共享是一个具有挑战性的问题。 


总体实现思路

    分布式 Session框架的架构图如图 10-9所示。

    为了达成上面所说的几个目标,我们需要一个服务订阅服务器,在应用启动时可以从这个订阅服务器订阅这个应用需要的可写Session项和可写 Cookie项,这些配置的 SessionCookie可以限制这个应用能够使用哪些 SessionCookie,甚至可以控制SessionCookie可读或者可写。这样可以精确地控制哪些应用可以操作哪些 SessionCookie,可以有效控制Session 的安全性和 Cookie的数量。 
 


    如 Session的配置项可以为如下形式: 

<session><key>sessionID</key><cookiekey>sessionID</cookiekey ><lifeCycle>9000</lifeCycle><base64>true</base64></session >

        Cookie的配置可以为如下形式: 

<cookie><key>cookie</key><lifeCycle></lifeCycle><type>1</type><path>/wp</path><domain>xulingbo.net</ domain><decrypt>false</decrypt><httpOnly>false</ httpOnly ></cookie>

    统一通过订阅服务器推送配置可以有效地集中管理资源,所以可以省去每个应用都来配置Cookie,简化Cookie的管理。如果应用要使用一个新增的Cookie,则可以通过一个统一的平台来申请,申请通过才将这个配置项增加到订阅服务器。如果是一个所有应用都要使用的全局Cookie,那么只需将这个Cookie通过订阅服务器统一推送过去就行了,省去了要在每个应用中手动增加Cookie的配置。

    关于这个订阅服务器现在有很多开源的配置服务器,如Zookeeper集群管理服务器,可以统一管理所有服务器的配置文件。

    由于应用是一个集群,所以不可能将创建的Session都保存在每台应用服务器的内存中,因为如果每台服务器有几十万的访问用户,那么服务器的内存肯定不够用,即使内存够用,这些Session也无法同步到这个应用的所有服务器中。所以要共享这些Session必须将它们存储在一个分布式缓存中,可以随时写入和读取,而且性能要很好才能满足要求。当前能满足这个要求的系统有很多,如MemCache或者淘宝的开源分布式缓存系统 Tair都是很好的选择。

    解决了配置和存储问题,下面看一下如何存取SessionCookie

    既然是一个分布式 Session 的处理框架,必然会重新实现 HttpSession的操作接口,使得应用操作 Session 的对象都是我们实现的 InnerHttpSession对象,这个操作必须在进入应用之前完成,所以可以配置一个filter拦截用户的请求。

    先看一下如何封装 HttpSession 对象和拦截请求,图 10-10是时序图。

    我们可以在应用的 web.xml 中配置一个 SessionFilter,用于在请求到达MVC 框架之前封装 HttpServletRequestHttpServletResponse对象,并创建我们自己的 InnerHttpSession对象,把它设置到requestresponse对象中。这样应用系统通过 request.getHttpSession()返回的就是我们创建的InnerHttpSession对象了,我们可以拦截 response addCookies设置的 Cookie

    在时序图中,应用创建的所有 Session 对象都会保存在 InnerHttpSession对象中,当用户的这次访问请求完成时,Session框架将会把这个 InnerHttpSession的所有内容再更新到分布式缓存中,以便于这个用户通过其他服务器再次访问这个应用系统。另外,为了保证一些应用对Session 稳定性的特殊要求,可以将一些非常关键的 Session再存储到 Cookie中,如当分布式缓存存在问题时,可以将部分Session 存储到 Cookie中,这样即使分布式缓存出现问题也不会影响关键业务的正常运行。 



    还有一个非常重要的问题就是如何处理跨域名来共享Cookie的问题。我们知道 Cookie  是有域名限制的,也就是在一个域名下的Cookie 不能被另一个域名访问,所以如果在一个域名下已经登录成功,如何访问到另外一个域名的应用且保证登录状态仍然有效,对这个问题大型网站应该经常会遇到。如何解决这个问题呢?下面介绍一种处理方式,如图10-11 所示。 



    从图中可以看出,要实现 Session 同步,需要另外一个跳转应用,这个应用可以被一个或者多个域名访问,它的主要功能是从一个域名下取得sessionID,然后将这个sessionID 同步到另外一个域名下。这个sessionID 其实就是一个 Cookie,相当于我们经常遇到的JSESSIONID,所以要实现两个域名下的Session 同步,必须要将同一个 sessionID作为Cookie写到两个域名下。

    总共 12步,一个域名不用登录就取到了另外一个域名下的Session,当然这中间有些步骤还可以简化,也可以做一些额外的工作,如可以写一些需要的Cookie,而不仅仅是传一个sessionID

    除此之外,该框架还能处理 Cookie 被盗取的问题。如你的密码没有丢失,但是你的账号却有可能被别人登录的情况,这种情况很可能就是因为你登录成功后,你的Cookie被别人盗取了,盗取你的Cookie的人将你的 Cookie加入到他的浏览器,然后他就可以通过你的Cookie正常访问你的个人信息了,这是一个非常严重的问题。在这个框架中我们可以设置一个Session签名,当用户登录成功后我们根据用户的私密信息生成的一个签名,以表示当前这个唯一的合法登录状态,然后将这个签名作为一个Cookie在当前这个用户的浏览器进程中和服务器传递,用户每次访问服务器都会检查这个签名和从服务端分布式缓存中取得的Session重新生成的签名是否一致,如果不一致,则显然这个用户的登录状态不合法,服务端将清除这个sessionID在分布式缓存中的 Session 信息,让用户重新登录。 


Cookie 压缩 

        CookieHTTP的头部,所以通常的 gzip deflate针对 HTTP Body的压缩不能压缩Cookie,如果Cookie的量非常大,则可以考虑将 Cookie 也做压缩,压缩方式是将 Cookie的多个k/v 对看成普通的文本,做文本压缩。压缩算法同样可以使用gzip deflate算法,但是需要注意的一点是,根据Cookie的规范,在 Cookie中不能包含控制字符,仅能包含ASCII码为 34~126的可见字符。所以要将压缩后的结果再进行转码,可以进行Base32或者Base64编码。

    可以配置一个 Filter在页面输出时对 Cookie进行全部或者部分压缩,如下代码所示: 

private void compressCookie(Cookie c, HttpServletResponse res) {try {ByteArrayOutputStream bos = null;bos = new ByteArrayOutputStream();DeflaterOutputStream dos = new DeflaterOutputStream(bos);dos.write(c.getValue().getBytes());dos.close();System.out.println("before 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 (IOException e) {e.printStackTrace();}}

    上面的代码是用 DeflaterOutputStreamCookie进行压缩的,Deflater压缩后再进行BASE64编码,相应地用 InflaterInputStream进行解压。 

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 (Exception e) {e.printStackTrace();}}

        2KB大小的 Cookie在压缩前与压缩后的字节数相差 20%左右,如果你的网站的Cookie2~3KB左右,一天有 1亿的 PV,那么一天就能够产生4TB 的带宽流量了,从节省带宽成本来说压缩还是很有必要的。 


表单重复提交问题 

    在网站中有很多地方都存在表单重复提交的问题,如用户在网速慢的情况下可能会重复提交表单,又如恶意用户通过程序来发送恶意请求等,这时都需要设计一个防止表单重复提交的机制。

    要防止表单重复提交,就要标识用户的每一次访问请求,使得每一次访问对服务端来说都是唯一确定的。为了标识用户的每次访问请求,可以在用户请求一个表单域时增加一个隐藏表单项,这个表单项的值每次都是唯一的token,如: 

<form id=”form” method=”post”><input type=hidden name=“crsf_token” value=“xxxx”/> </form>

    当用户在请求时生成这个唯一的 token 时,同时将这个 token保存在用户的 Session中,等用户提交请求时检查这个 token 和当前的 Session中保存的 token是否一致。如果一致,则说明没有重复提交,否则用户提交上来的token已经不是当前这个请求的合法 token。其工作过程如图10-12所示。 



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



    当用户提交表单时会将请求时生成的token带回来,这样就可以和在 Session 中保存token 做对比,从而确认这次表单验证是否合法。 


多终端 Session 统一 

    当前大部分网站都有了无线端,对无线端的Cookie如何处理也是很多程序员必须考虑的问题。

    在无线端发展初期,后端的服务系统未必和PC 的服务系统是统一的,这样就涉及在一端调用多个系统时如何做到服务端Session共享的问题了。有两个明显的例子:一个是在无线端可能会通过手机访问无线服务端系统,同时也会访问PC 端的服务系统,如果它们两个的登录系统没有统一的话,将会非常麻烦,可能会出现二次登录的情况;另一个是在手机上登录以后再在PC 上同样访问服务端数据,Session能否共享就决定了客户端是否要再次登录。

    针对这两种情况,目前都有理想的解决方案。

        1)多端共享Session

    多端共享 Session必须要做的工作是不管是无线端还是PC 端,后端的服务系统必须统一会话架构,也就是两边的登录系统必须要基于一致的会员数据结构、CookieSession的统一。也就是不管是PC 端登录还是无线端登录,后面对应的数据结构和存储要统一,写到客户端的Cookie也必须一样,这是前提条件。 

    那么如何做到这一点?就是要按照我们在前面所说的实现分布式的Session框架。如下图 10-14所示。 



    上面服务端统一 Session后,在同一个终端上不管是访问哪个服务端都能做到登录状态统一。例如不管是Native还是内嵌Webview,都可以拿统一的Session ID去服务端验证登录状态。

        2)多终端登录
    目前很多网站都会出现无线端和
PC端多端登录的情况,例如可以通过扫码登录等。这些是如何实现的呢?其实比较简单,如图10-15 所示。 



    这里手机端在扫码之前必须是已经登录的状态,因为这样才能获取到底是谁将要登录的信息,同时扫码的二维码也带有一个特定的标识,标识是这个客户端通过手机端登录了。当手机端扫码成功后,会在服务端设置这个二维码对应的标识为已经登录成功,这时PC客户端会通过将“心跳”请求发送到服务端,来验证是否已经登录成功,这样就成为一种便捷的登录方式。 


总结 

        CookieSession都是为了保持用户访问的连续状态,之所以要保持这种状态,一方面是为了方便业务实现,另一方面就是简化服务端的程序设计,提高访问性能,但是这也带来了另外一些挑战,例如安全问题、应用的分布式部署带来的Session的同步问题及跨域名 Session 的同步问题等。本章分析了 CookieSession的工作原理,并介绍了一种分布式 Session 的解决方案。 


0 0
原创粉丝点击