上一章讲解了HTTP Session和HTTP Cookie的基本概念,这一章,我们通过具体例子,深化对HTTP的理解,从而掌握其要点。
我们先来看一个访问一个技术网站的例子。这个技术网站叫做theserverside.com。
我们在浏览器中访问www.theserverside.com这个网址。我们的浏览器是支持Session Cookie的。浏览器会发出如下的HTTP Request。注意,为了节省篇幅,我把不重要的Request Headers都去掉了。
GET / HTTP/1.1
….
Host: www.theserverside.com
….
请注意,这个HTTP Request里面没有cookie header。
theserverside.com网站接到这个请求,返回了如下的HTTP Response。
HTTP/1.1 200 OK
……
Set-Cookie: JSESSIONID=152468EB452162CB6E8A1170ED0B02F1; Path=/
….
1df3
….
请注意,在Response Headers部分,有一个set-cookie的Response Header。这是因为,网站服务器没有从浏览器的HTTP Request发现cookie,自然也没有发现Session ID。于是,它就分配了一个Session ID,通过set-cookie header传给浏览器。
在同一个浏览器中,我再一次访问这个网站。浏览器这次发出的HTTP Request如下。
GET / HTTP/1.1
……
Host: www.theserverside.com
……
Cookie: JSESSIONID=152468EB452162CB6E8A1170ED0B02F1
这一次,浏览器把含有Session ID的cookie发过去了。这一次,网站服务器没有发出set-cookie header,因为它已经知道,浏览器那边已经存放了Session ID。
从这个例子中,我们就可以看到网站服务器的Session ID分配原则。你没有Session ID,我就给你一个。你有了,我就不用给了,就用这一个。
关于Session ID的分配,存在这么一个原则。一个浏览器进程,和一个网站服务器之间,就对应着一个Session ID。
在服务器端,一个网站服务器中,一般都存在很多个Session ID。
一个浏览器进程,访问一个网站服务器,就会产生一个Session ID。
N个浏览器进程,访问同一个网站服务器,就会产生N个Session ID。
在浏览器端,一个浏览器中,也可能存在多个Session ID。
一个浏览器进程,访问一个网站服务器,就会产生一个Session ID。
一个浏览器进程,访问N个网站服务器,就会产生N个Session ID。
下面看具体的例子。
例1:
在这个例子中,一个浏览器进程访问三个网站:a.com,b.com,c.com。
在浏览器端,Session Cookie中,就存在着三个Session ID:a.com网站分配的x001;b.com网站分配的yy001;c.com网站分配的zzz001。
例2:
在这个例子中,三个浏览器进程访问同一个网站:a.com。
在服务器端,就存在着三个Session ID:x001,x002,x003。
例3:
在下面的例子中,有三个浏览器进程,有三个网站。每个浏览器进程都访问那三个网站。总够就有3 * 3 = 9 个Session ID。
关于Session ID,还有一种有趣的场景——在一些HTML中,可能会包括多个Frame(一种HTML元素),每个Frame都有自己对应的网址。那么,这些Frame的Session ID是怎样一种状况呢?
原则是一样的。如果Frame对应的网站地址是一样的,那么,就共享同一个Session ID。不同的网站,就是不同的Session ID。
浏览器端只负责存放Session ID,服务器端不仅负责存放Session ID,还通常维护一个对应Session ID的Session状态存储空间,有时候也简称为服务端Session空间。Session状态存储空间都有些什么内容呢?什么内容都可能有,程序员什么东西都可以往里面塞。这个问题应该换个问法。Session状态存储空间应该放些什么内容呢
我的建议是,最好什么都不要放,最多只放用户的登录信息。为什么尽量避免在Session状态存储状态空间中放东西?这一点,前面已经讲过了,不再赘述。因为,在集群环境中,Session中的状态,很可能会通过网络,发送到其他的计算机。如果Session状态过重,就会加重系统的负担,影响系统的性能。所以,千万不要养成随时把临时数据扔进Session存储空间的习惯。
所以,一定要牢记这个原则。服务器Session空间中只应该放用户登录信息。那么,用户登录到底是什么意思?又和Session ID有什么关系呢?
Session ID是浏览器身份的识别。而用户名登录则是人的身份的识别。用户名登录是建立在Session ID基础上的。如果服务器端没有给浏览器分配Session ID,那么,用户就不能用这个浏览器登录网站服务器。如果服务器端给浏览器分配了一个Session ID,就说明,服务器端同时也给这个浏览器预留了一个用来存放Session状态的存储空间。这个Session状态存储空间就可以用来存放用户的登录信息。
当用户在网站的登录界面上输入用户名和密码,进行登录的时候,浏览器就会把包含了用户名和密码的HTTP Request发送到服务器端。服务器端收到了用户名和密码,同时还收到了Session ID。
网站服务器在数据中查询用户名和密码,如果匹配,用户就登录成功,网站服务器就会把用户名和对应的权限信息存放到Session ID对应的Session状态存储空间中。
用户的下一次请求过来的时候,网站服务器就会根据用户的Session ID,去找对应的Session状态存储空间,从中取出用户名和对应的权限信息,以便决定用户可以使用的服务内容。
如果说,Session ID只是网站服务器提供的普通服务的话,那么,用户名登录就是网站服务器提供的高级会员服务。
前面讲过,一个浏览器进程,一个网站地址,对应一个Session ID,对应一个服务器端的Session空间。这就是说,一个浏览器进程,只能用一个用户名登录同一个网站。
如果,我们在同一个网站上有多个用户名,我们想同时登录,应该怎么办呢?这就需要使用多个浏览器进程。这就依赖于浏览器的进程模式了。
在真实的世界中,不同的浏览器,有不同的实现方案。
有些浏览器(比如,目前的微软IE浏览器)同时支持多进程和多线程。你直接运行该浏览器多次,就产生多个浏览器进程,每个浏览器都有自己的Session Cookie,互不干涉。
这样,你可以用多个浏览器进程访问同一个网站,并且用多个不同的用户名登录。
当你直接运行一个浏览器,并且从该浏览器的菜单中又启动了一个浏览器的时候,实际上相当于多创建了一个浏览器线程。这两个浏览器线程共享同一个进程空间,因此也共享同一份Session Cookie。他们访问同一个网站,只能用一个用户名登录。
还有的浏览器只支持单进程多线程模型,无论你创建多少个浏览器,都是同一个进程的多个线程。这些浏览器共享同一个进程空间,因此也共享同一份Session Cookie。他们访问同一个网站,只能用一个用户名登录。在同一个网站多个用户名的情况下,这种浏览器就极为不方便。
还有一种浏览器(比如,目前的Google浏览器),支持多进程模型。每一个浏览器都是一个进程。按理来说,这些浏览器都有自己的进程的空间和Session Cookie,因此可以用不同的用户名登录同一个网站,但是,不行。因为这种浏览器的多个进程之间,竟然是共享Session Cookie的。他们只能用一个用户名。
还有些浏览器(比如,目前的Firefox浏览器),可以定义不同的用户空间,虽然稍微麻烦了,但至少可以实现多用户登录。
用户登录是一个十分重要、又十分复杂的主题。本书不可能面面俱到,只能择起要点而阐述之。关于用户登录,无论是作为用户,还是作为开发人员,我们首要关注的问题都是账户安全问题。
我们知道,用户登录信息都是存放在服务器端的Session空间中的,而Session空间是对应Session ID,而Session ID是存放在HTTP Cookie Header里面的。如果HTTP Cookie Header被人截获,Session ID就可能被人截获并仿造,从而冒名登录到网站中。所以,用户登录时及登录之后,一定要采用HTTPS协议,一种基于HTTP的加密协议。这是最基本的保障。
除此之外,我们还要考虑用户的一些粗心大意的情况。比如,在登录网站之后,又开始做别的事情,之后又把登录网站这件事给忘了,开着浏览器就走了。这时候,如果别人过来,使用浏览器的话,就可以直接使用已经登入的账号。
为了避免这个问题,网站引入了Session过期机制。当用户登录之后,长期不操作,网站就会让Session过期(Time Out)。Session过期的时间限制由网站自行定义。对于隐私保护严的账户,比如银行账户,过期时间就短一些,比如,5分钟。对于隐私保护不是那么严的账户,过期时间就会长一些。
当Session ID过期之后,Session ID对应的Session状态存储空间也会过期,里面存放的用户信息也会过期,用户登录状态也会过期。这时候,就需要重新登录。
这就能解决所有问题吗?不能。在Session过期的情况下,如果浏览器仍然开着,不需要知道用户名和密码,一样有办法登录回去。只要回退到之前的登录界面——通常是一个HTML Form界面。由于浏览器通常会缓存HTTP Request的信息,只要刷新一下,忽略浏览器的警告“表单信息已经过期,您确定要重新提交吗?”,就可以重新登录了。
浏览器的这种缓存功能有时候会造成严重的后果。比如,你在一个公共场合,用浏览器访问了一个网站上的私人账户。然后,你退出了登录,然后离开了。但是没有关浏览器。另一个人走过来,把浏览器回退到你之前的登录界面,一刷新,刷,又登录进去了。所以,一定要切记,用完私人账户之后,一定要随手关闭浏览器。
有些安全做得比较好的网站,就考虑到了这一点。它想方设法不让用户退回到之前的登录界面。其手段包括但不限于Refresh(刷新)和Redirect(重定向)。
W3网站上有一段关于Refresh vs Redirect的内容。
http://www.w3.org/QA/Tips/reback
里面是这么讲的。请不要使用Refresh(刷新),因为,Refresh会打断用户的回退按钮。
Refresh是这样一种技术。假设http://www.example.org/foo返回的HTML页面中包含如下的元描述:
.
那么,过1秒钟,该页面就会自动跳转到http://www.example.org/bar。
当用户想回到前一个页面的时候,刚退回去,一秒之间,页面又跳转了。这样,就成功地打断了回退操作。用户会感到很恼火,一下就把浏览器关了。
为了避免这种情况发生,最好用HTTP Response Redirect,就不会打断浏览器的回退操作。
这里面,能够打断回退操作的主要技术手段是Refresh。但我们可以把Refresh和Redirect组合使用。比如,用户登录之后,网站可以先把用户Redirect到一个包含了Refresh Meta元素的HTML页面,让浏览器再次自动跳到别的页面,然后再Redirect,再Refresh,如此多次,最后显示登录后的页面。这个登录之后的页面URL已经和登录界面的URL毫无关系了。
如果用户想退回到之前的登录界面,就会不断地遭遇到Refresh和Redirect,回退操作被打断得支离破碎,用户就会就会遭遇到极大的挫折感,愤而关闭浏览器,这就达到了保护账户安全的目的。
这是安全机制做得好的网站,大部分网站做得没有这么周到。所以,我们一定要自己注意,在访问私人账户之后,一定要随手关闭浏览器。在关闭浏览器之前,最好点一下退出按钮,强制清空服务器端的Session状态存储空间的用户信息。这样就更保险。
在前面那个打断登录界面和登录后界面的回退操作的用例中,Refresh起到了主要的破坏作用,Redirect只是起了一点辅助作用的帮凶。事实上,Redirect的长处不在于破坏,而在于建设。一些复杂的登录机制,就借用了Redirect的威力。
比如,我们在访问网站资源的时候,经常会遇到这样的情况:网站弹出一个对话框,提示“您访问的资源,需要权限认证,请您先登录”,并且提供了用户名和密码输入框,让用户登录。
等我们输入用户名和密码之后,网站弹出“恭喜您成功登录”,然后,页面就转向之前访问的资源页面。其实现原理也是基于HTTP协议,确切来说,是基于Request Header和Response Header。
Reference这个Request Header的作用是记录用户访问的上一个网址,即用户是从哪里链接过来的。通过Reference这个Request Header,网站服务器就可以知道用户访问的上一个页面是什么,就可以找个地方暂时记录下来,等用户登录成功之后,再通过HTTP Response 把页面Redirect到之前记录下来的网址。
那么,这个网址暂时存放在哪里好呢?最方便的地方,自然是服务端Session存储空间,而且语义上也很直观——该用户访问过的页面,就应该放到该用户的Session存户空间当中。
但是,我们前面讲过了。千万不要养成这个习惯。不过,如果您坚持这个习惯,就当我什么也没说。
不放在Session里面,那应该放在哪里?读者可能会问了。我的建议是,放到一个缓存中。非集群环境下,就放到本机的缓存中。集群环境下,就放到中心缓存服务器中。其效果和放在Session里面是一样的。这种做法虽然多了几步麻烦,但是,也避免了很多潜在麻烦。
需要特别注意的是,上面的场景中,Redirect指令是服务器端发给浏览器的指令,是通过HTTP Response的Redirect Header实现的。这个概念一定要理解清楚。
Reference和Redirect组合使用的应用场景十分广泛,比较复杂的应用场景就是单点登录(Single Sign On)。
单点登录的场景一般是这样的。在一个网络中——有可能是局域网或者互联网,有多个网站服务器。每个服务器都提供不同的服务,比如,博客,新闻,论坛,等。这些网站服务器共享同一份用户登录管理系统和同一个数据库服务器(即共享同一份用户数据)。
由于每个服务器都有各自的Session存储空间,因此,每个网站都需要用户单独登录,而且每个网站都使用相同的用户名和密码登录。
有没有一种方案,能让用户只登录其中一个网站,就可以自动登录其他网站呢?
有。这种方案就是单点登录。
我们首先考虑最简单的场景——主域名相同的多个服务器。比如:
news.a.com
blog.a.com
forum.a.com
这三个网站虽然都有各自的网址,但他们的主域名——a.com,是相同的。这三个网站虽然在同一个局域网内,但他们都有独立的外网IP地址,都是直接暴露在互联网上的。
前面讲过,网站设置cookie的时候,可以把cookie的作用域设置为上级域名。这种情况下,就可以定义一个对应a.com的Cookie,来实现单点登录。当浏览器成功登录到某一个网站的时候,就会收到一个对应a.com的Cookie,里面存放了用户名和密码。下一次,浏览器访问另一个网站的时候,就把a.com对应的Cookie发送到该网站。该网站往里一看,哦,已经登录过了,还有用户名和密码,那就自动登录一下吧。这就登录了。
这是最简单的情况,还不需要借用Redirect的威力。当主域名不相同的时候,就是Redirect大显身手的时候了。跨域名的通用单点登录方案就是:Redirect + 中心认证服务器。
什么叫中心认证服务器呢?就是说,在一个单点登录的网站群组中,所有的用户登录都要经过同一个服务器,这个服务器就叫做中心认证服务器。中心认证服务器的作用,相当于多个网站之间共享的服务端Session空间。
下面举一个具体的例子,来说明单点登录的整个流程。
当用户访问单点登录群组中一个网站a.com时,网站a.com先为用户浏览器分配一个Session ID,叫做a001。然后,网站a.com查看该用户的HTTP Request的Cookie Header里面是否存在中心认证服务器颁发的一个登录凭据(英文叫做ticket)。
如果登录凭据不存在,说明该用户是第一次访问该群组。网站a.com就把用户页面Redirect到中心认证服务器auth.com。
auth.com先从HTTP Request的Reference Header中,把a.com这个网址存下来,然后,显示一个登录页面给用户。
当用户输入用户名和密码,成功登录到中心认证服务器auth.com之后,中心认证服务器生成一个登录凭据,不妨叫做a.com-ticket001。
中心认证服务器相当于一个服务端Session空间,不过是多个网站之间共享的服务端Session空间。a.com-ticket001就相当于一个Session ID,不过是多个网站之间共享的Session ID。
在单个网站的例子中,网站服务器会为每一个Session ID分配一个Session存储空间,用来存放登录信息。同样,中心认证服务器auth.com也会为每一个登录凭证(ticket)开辟一个存储空间,不妨称为Ticket存储空间,用来存放登录信息——用户名和密码。
中心认证服务器auth.com会为a.com-ticket001分配一个Ticket存储空间,叫做a.com-ticket001存储空间,里面可以用来存放用户登录信息,比如用户名和密码。
做完这些之后,中心认证服务器auth.com做两件事情。第一件事是访问a.com,把ticket= a.com-ticket001这个参数发给网站a.com。
这个工作可以通过HTTP协议完成,比如,中心认证服务器auth.com可以访问网址a.com? ticket=a.com-ticket001。网站a.com收到ticket=a.com-ticket001这个参数之后,a.com-ticket001就把存在自己的登录凭据库中。
做完第一件事之后,中心认证服务器auth.com做第二件事,为浏览器准备了HTTP Response。这个HTTP Response是一个Redirect指令,包含了Redirect Header。Redirect的目的网址为:
a.com?ticket=ticket001。
另外,中心认证服务器auth.com还在HTTP Response中为用户浏览器准备了set-cookie header。里面放置了信息,ticket=a.com-ticket001。
用户浏览器收到了这个HTTP Response之后,先把HTTP Response里面的set-cookie header中的信息ticket=a.com-ticket001存放到Session Cookie中,生成一个“auth.com-> cookie : ticket = a.com-ticket001”的条目。
然后,用户浏览器根据HTTP Response的Redirct指示,去访问a.com?ticket=ticket001。
这一次,网站a.com在HTTP Request的网址参数中找到了ticket这个参数。网站a.com就知道,该用户浏览器已经在中心认证服务器auth.com登录认证过了。网站a.com会去中心认证服务器auth.com那里核对一下。如果核对成功,就表示用户确实登录认证了。
网站a.com在服务端的Session ID = a001 的Session存储空间中,存放用户登录认证状态,表示该用户浏览器已经登录过了。然后,网站a.com在HTTP Response的set-cookie header里面也设置一条ticket=a.com-ticket001信息(或者其他形式的登录成功信息),并把受认证保护的页面返回给用户浏览器。
用户浏览器收到页面的同时,也从HTTP Response里面获得了cookie信息。这时候,用户浏览器的Session Cookie中就包括如下信息:
auth.com -> cookie : authSessionID = xxx; ticket = a.com-ticket001。
a.com -> cookie : aSessionID = a001; ticket = a.com-ticket001。
这样,下次用户浏览器在访问a.com和auth.com的时候,a.com和auth.com就可以判断出用户浏览器已经登录认证过了。
注意,用户浏览器cookie中的ticket = a.com-ticket001信息,只是一种表示“已登录状态”的简明表达。在真实的实现中,出于安全等方面的考虑,不一定采用这种表达形式。
当用户浏览器访问单点登录群组中的另一个网站b.com的时候。b.com也会为用户浏览器分配一个bSessionID=b001的Session ID,并且在服务器端分配一个对应b001的Session空间。
b.com没有找到用户浏览器的登录认证状态,同a.com一样,把浏览器页面Redirect到中心认证服务器auth.com。这时候,auth.com从用户浏览器的cookie中发现了如下信息:
cookie : authSessionID = xxx; ticket = a.com-ticket001。
说明,该用户浏览器已经登录过了。于是,auth.com的行为和和前面一样,为b.com生成一个ticket = b.com-ticket001的登录凭据。并通知b.com。然后把用户页面Redirect到 b.com ? ticket=b.com-ticket001。
b.com收到用户浏览器的请求,就知道用户浏览器已经登录认证,核对之后,就set-cookie :
ticket = b.com-ticket001。
浏览器收到b.com的HTTP Response之后,其Session Cookie中的条目如下:
auth.com -> cookie : authSessionID = xxx; ticket = a.com-ticket001; ticket = b.com-ticket001。
a.com -> cookie : aSessionID = a001; ticket = a.com-ticket001。
b.com -> cookie : bSessionID = a001; ticket = b.com-ticket001。
这样,就等于用户浏览器在auth.com、a.com、b.com都登录了。
以上只是一种简化的、示意说法。只是为了大致说明工作原理流程,并不严密。真实的实现方案多种多样,有可能在某些环节更简单,有可能再某些环节更复杂,还需要考虑各种各样的现实问题。不同的单点登录实现方案可能大相径庭,请读者一定要注意这一点。