HTTP协议之用户识别(四)

HTTP协议是无状态的,这意味着每次的请求/响应都是独立进行的,也就是说服务器无法区分两次请求是否来自同一个用户(客户端)。但是在Web网页和应用中,服务器可能同时与多个客户端进行对话, ,因此区分用户身份信息是很有必要的。

<!--more-->

1. 基础用户识别

在之前的项目中,后台都是通过用户从登陆表单提交的信息来辨别用户身份的,这么做事理所应当的,但是却没有仔细思考过“HTTP是无状态的”这一机制。HTTP本本并不会记录之前传输过的信息,也就是说即使用户已经提交了账号密码,但是在下一次请求中,服务器仍旧不能辨别该用户的身份。

关于用户识别这个问题,有篇很有趣的例子,以咖啡店记录某位顾客的消费记录为例,列举了用户识别的三种解决方案:

  • 由协议本身记录用户身份状态(HTTP报文)
  • 在客户端记录用户身份,并在每次的请求报文中向服务器表明身份(Cookie)
  • 直接在服务端记录用户身份(Session)

1.1. 首部行识别

HTTP协议是无状态的,但是协议本身提供了一些用来承载用户相关信息的请求报文首部行,通过这些首部行提供的信息,服务器多多少少可以得到用户的一些信息:

  • From,可以携带用户的Email地址,但是处于隐私的保护,浏览器很少发送该首部行
  • User-Agent,包含请求浏览器的相关信息,有时甚至还包括操作系统的相关信息,这个请求首部行在请求报文中的出现频率是十分高的,但是,浏览器的相关信息对于服务器了解用户的特定身份并没有多大意义
  • Referer,表明用户是从点击进入发送当前请求的那个文档,可以用来统计站点的用户来源,也可以防盗链(比如访问来源不是本站,则不显式当前页面),在JavaScript中可以通过document.referrer来获取(这里两个单词不一样,但是含义相同),使用实践参考张鑫旭-document.referrer实践,这里就不展开了

1.2. IP识别

HTTP协议是建立在TCP/IP之上的,Web服务器可以在三次握手阶段找到客户端的IP地址和端口号,并将IP地址作为用户身份识别的一种依据。 毫无疑问,这种做法是可行的,但是存在众多问题:

  • 用户连接网络的IP可能是服务商动态分配的
  • 出于安全性的考虑,许多用户都通过防火墙来连接网络,防火墙会将实际的客户端IP转换成共享的防火墙IP地址(以端口号进行区分)
  • HTTP代理和网关会建立一些新的到服务器的TCP连接,服务将只看见代理服务器的IP地址而不是实际的客户IP地址(可以在请求报文中添加Client-IP扩展首部行来表明)

无法用IP确定目标的地方来多了,因此这并不是一种可靠的用户识别的办法(想起之前某个项目由于兼容的问题还是用IP来定位~)

1.3. 其他

除了上面介绍的两种方式,还有依靠用户登录和利用URL追踪用户身份的方式,我遇见的情形很少,这里就不折腾了,用户识别的重头戏是Cookie

Cookie是服务器发送到用户浏览器并保存在浏览器上的一块数据,它会在浏览器下一次发起请求时被携带并发送到服务器上。Cookie的使用使得基于无状态的HTTP协议上记录稳定的状态信息成为了可能。

由于Cookie可以由服务器自定义用户身份信息,因此在很长一段时间内都是识别用户身份,实现持久会话的最佳方式。打开开发者工具,切换到Application面板,在左侧导航选择Cookies,就可以查看当前页面域名下所保存的所有cookie。

2.1. 工作原理

服务器希望能够识别再次访问的用户,因此,

  • 当用户首次访问网站时,服务器为该用户生成了一系列name=value的信息列表,并在响应时把这些信息发送给浏览器。
  • 浏览器接受到服务器的cookie返回值之后,会提取响应报文中的cookie内容并保存在本地(不同的浏览器保存方式可能不同)
  • 当浏览器再次访问同一服务器时,浏览器自动从本地提取之前保存的该服务器的cookie,然后将这些信息附在请求报文中发送给服务器
  • 服务器通过提取请求请求报文的cookie,从而获取客户端的身份信息。

这里可以看出,浏览器负责接收和积累服务器的特定信息,并对来自不同服务器的cookie进行区分,此外在服务器指定Cookie以后浏览器的每次请求都会携带Cookie数据,感觉所有脏活累活都交给浏览器来干了,因此cookie系统被称为客户端侧状态,规范的正式名称是“HTTP状态管理机制”。

Cookie非常依赖于浏览器,不同浏览器之间的Cookie是不能通用的。可以笼统地将Cookie分为两类:会话Cookie和持久Cookie:

  • 会话Cookie是临时Cookie,用户退出浏览器时Cookie就被删除了(并不是关闭标签页就删除哦)
  • 持久Cookie是指会被保存在硬盘上的Cookie信息,浏览器退出,计算机重启时他们仍然存在。

会话Cookie与持久Cookie的区别在于是否为他们设置了过期时间(马上就会提到),如果没有,则为会话Cookie。接下来看看服务器是如何在响应报文中设置Cookie的。

2.2. 服务端的cookie

服务器使用Set-Cookie响应首部行向浏览器发送请求信息,下面是我随便复制的一份响应报文中的cookie信息:

Set-Cookie:dwf_sg_task_completion=False; expires=Tue, 18-Apr-2017 09:25:31 GMT; Max-Age=2592000; Path=/; secure

Set-Cookie:dwf_section_edit=False; expires=Tue, 18-Apr-2017 09:25:31 GMT; Max-Age=2592000; Path=/; secure

基本格式 可见,一个响应报文中可以包含多条Set-Cookie首部行,单条Set-Cookie首部行由一个必须的键值对,后跟上数条可选的修饰词组成。其基本格式为:

Set-Cookie: <cookie-name>=<cookie-value>; [Domain=<domain-value>;][Secure;][HttpOnly;][Expires=<date>][Max-Age=<non-zero-digit>][Path=<path-value>]

参考Set-cookie文档

信息键值对 <cookie-name>=<cookie-value>这条信息键值对是该首部行必须的,因为cookie本身就是传送信息的。其中,namevalue都是字符序列,除非包含在双引号中,否则不宝库奥分号,逗号,等号和空格等。

修饰符 cookie中的修饰符是可选的,用来设置这条cookie的相关性质,便于浏览器进行处理,下面列举了部分修饰符

  • Domain=<domain-value>, 控制哪些站点可以看到该cookie
  • Path=<path-value>,为服务器上的特定文档指定的独立cookie
  • Expires=<date>,指定一个日期字符串作为Cookie的过期日期,设置了过期时间的cookie会成为一个持久cookie

在RFC2959规范中引入了一个cookie的扩展版本,包含一个Set-Cookie2的首部行,下面列举了部分修饰符:

  • Max-Age=<non-zero-digit>,属性值为一个整数,用于设置以秒为单位的cookie有效时间,设置了过期时间的cookie会成为一个持久cookie
  • Secure,表示只有在HTTP使用SSL安全连接时才发送该cookie
  • HttpOnly,表示该Cookie无法被客户端Javascript操作

但是从上面摘抄的那两条首部行可以发现,尽管带有Secure属性,却是出现在Set-Cookie首部行中,这个...

后台语言 当然,后台语言基本都提供了设置cookie的函数或方法,在php中,可以使用setcookie函数在响应报文中设置cookie,使用$_COOKIE$_REQUEST来提取请求报文中的cookie

setcookie(name,value,expire,path,domain,secure);
var_dump($_COOKIE);

2.3. 客户端的cookie

与响应报文中Set-Cookie相对应的是请求报文中的Cookie首部行。与Set-Cookie2相对应的是Cookie2,具体的版本协商由客户端和服务器自行决定。 在请求报文中,请求站点对应的多条信息列表被连接在一起,与Set-Cookie不同的是,传回服务器的Cookie信息不需要修饰符,

Cookie: name=value; name2=value2; name3=value3

浏览器会自动处理所保存的相关cookie并添加到请求报文中,因此这里不需要我们做任何处理。然而在客户端,我们也可以通过JavaScript来操作cookie

// 存储数据
document.cookie = "name = value";
// 为cookie添加修饰符
document.cookie = "name = value;expires = " + (new Date).toGTMstring();
// 移除cookie
document.cookie = "name = value;expires = -1 ";

// 获取当前网页cookie,得到的是一个字符串的值,包含当前网站下所有的cookie
var cookies = document.cookie;
// 因此提取单个cookie要麻烦一点,需要先拆分然后再遍历
var cookiesArr = cookies.split(";")

需要注意的是,通过设置过期时间移除cookie过期,只是将,只是从一个持久cookie变成了一个会话cookie,因此,需要关闭浏览器再重新进入,才能看见cookie被真正移除的效果。

当然,如果服务器在Set-Cookie中添加了HttpOnly,则客户端的Javascript就无法操作这个Cookie了。

2.4. 小结

这里先从cookie的工作原理开始,再分别从Set-CookieCookie首部行的结构,了解了服务器和浏览器是如何处理cookie的,这里只是整理了一些基础知识,还有待深入了解。

3. Session

浏览器在每次请求时都附带Cookie,通过在Cookie中包含相关的用户信息,服务器可以识别(读取Cookie)和跟踪用户(更新Cookie),相关的用户信息都是保存在客户端中的,因此也可以称为“客户端识别机制”。 由于需要在每次的请求中都附带用户信息,如果用户的身份信息和需要记录的状态比较复杂,为什么不直接在服务器直接保存用户信息呢?

3.1. 概念

在服务器保存用户身份的做法是可行的,这种方式称为Session,可以简单地理解为“服务端识别机制”。 如果只是在服务端保存用户信息是不够的,因此服务器还是不能区分某次请求对应的到底是哪个用户。回到篇头的问题,其大致思路大概是是在验证用户登录表单提交的账号和密码的合法性之后,为用户生成一个标识符(通常称为SessionID)并在服务器保存该用户的相关信息;然后服务器通过再次接收这个SessionID,识别出用户的身份并提取相关的用户信息。

上面这段话能够引出两个问题:

  • 如何传递SessionID并识别用户
  • 如何在服务端保存用户信息

3.2. SessionID

维持session会话的核心就是客户端的唯一标识,即SessionID,SessionID就像是用户的身份证账号一样,只需要提供值,服务器就会自动检索并查找到用户的身份信息。这决定了SessionID应该是一个唯一的值。此外,处于安全性的考虑,SessionID应该是一个很难被仿造的值(因为Cookie有浏览器的同源策略等安全机制保护,而Session只是用户在请求中附带一个标识符值,很容易进行伪造)。

服务器为用户生成了一个SessionID,必须通过某种方式传递给给用户,用户才能在下次请求时提供这个SessionID,然后才能进行下一步的认证。那么,怎样才能将这个标识符传递给客户端呢?

Cookie 首先,通过Cookie来传递SessionID是一种十分常见的做法。与使用Cookie保存用户信息不同的是,这里只需要使用Cookie传递一个标识符就可以了。只要接收到了这个值,在Cookie未过期的时间内,浏览器都会向服务器提供这次会话的SessionID。

但是,由于用户可以在浏览器设置中禁用Cookie,因此出于程序健全性的考虑,必须有能够在客户端cookie被禁止的情况下将SessionID传递回服务器。

通过URL参数 可以通过在服务器对URL进行改写,通过在请求URL上附带SessionID的方式进行传递,这种方式不需要通过cookie,但是却直接将SessionID暴露在请求历史记录中了。

通过表单隐藏字段 可以在服务器渲染页面模板时为表单增加一个隐藏字段input[type="hidden"],直接使用SessionID的值进行赋值,这么做依赖特定的页面结构,对于无表单结构的模板页面并不适用。

总之,之前项目中最常用的传递SessionID的做法还是使用Cookie。

3.3. 服务器保存信息

服务器使用类似于散列表的结构来保存多个用户的信息,每个用户的信息使用SessionID来索引,这就是SessionID必须惟一的原因。 用户信息可以保存在内存中,文件中或者数据库中(查了相关的资料,并没有很全的了解)。但是都会出现的问题时,如果采用session管理用户会话,当访问量过大时,无疑会明显增大服务器的压力,影响整个站点的性能(因为服务器既要处理正常的请求,又要识别用户的身份)。

关于Session更多的知识,应当更深入学习后端才行,这里就只是简单了解一下。