看完这篇,吃透Cookie和Session

从HTTP的无状态说起

目前的浏览器使用的协议都是HTTP协议,HTTP协议的特点之一就是无状态,即每条HTTP请求都是一条全新的请求,不依赖上一条或下一条HTTP请求。如何理解“无状态”?可以认为服务器是无记忆的。客户端和服务器永远是处在一种“无知”的状态。建立连接前两者互不知情,每次收发的报文也都是互相独立的,没有任何的联系。收发报文也不会对客户端或服务器产生任何影响,连接后也不会要求保存任何信息。可以说,服务端处理HTTP请求,他的记忆只在请求-响应这一段时间存在

请看下面这个例子

小明:我是小明,请你给我发一下A文件。

服务器:让我看看你有没有权限,有就发给你。

小明:再给我发送一下B文件。

服务器:让我看看你有没有权限,有就发给你。

小明:……

服务器不是已经早已检查过小明的权限了吗?怎么又检查。这就是HTTP的无状态。每个HTTP请求,服务器都会将其作为一个全新的请求,在HTTP协议中,你不会看到有任何字段会去标识HTTP请求的状态。他不像在下一层的TCP协议,一开始处于 CLOSED 状态,连接成功后是 ESTABLISHED 状态,断开连接后是 FIN-WAIT 状态,最后又是 CLOSED 状态。

这些“状态”就需要 TCP 在内部用一些数据结构去维护,可以简单地想象成是个标志量,标记当前所处的状态,例如 0 是 CLOSED,2 是 ESTABLISHED 等等。

无状态带来了什么问题?

如果仅仅是存储一些静态资源文件,请求来了读取完就走就行了,没什么关联操作。但随着 HTTP 应用领域的不断扩大,对“记忆能力”的需求也越来越强烈。例如论坛、电商、购物,都需要“看客下菜”,只有记住了用户的身份才能发帖子、下订单等一系列需要多次HTTP请求的业务。

问题:服务端无法判断HTTP请求来自哪一个用户

解决办法也很简单,利用HTTP的另外一个特点:可扩展性高。

标识:身份证

在我们一出生之后,会在某一个时间点去公安局办理一个身份证,此后这一辈子都会用哪一个身份证号了。无论是坐火车、高铁飞机,亦或者是办理入学、各种各样需要证明自己身份的地方,都需要展示它。

那么类似的,在我们第一次访问服务器的时候,服务器会给我们一张身份证,我们需要将它保存好,然后在需要的时候带上他,证明我们的身份。那么客户端该怎么保存身份证?又怎么带上它呢?每次都要这两个操作那能不能不用这么麻烦?

Cookie:保存身份证

好了,Cookie就是客户端数据保存技术。下面详细介绍Cookie的工作过程。

看完这篇,吃透Cookie和Session_第1张图片

  • 浏览器向服务端的HTTP请求接口发起HTTP请求,服务端在响应处设置cookie
  • 响应回来时,浏览器自动将Cookie保存到浏览器
  • 接下来的一段时间内,每次向服务端进行请求时都会带上Cookie
HTTP响应头:带回身份证

在图中我们可以看到,当浏览器向服务端发送第一次请求后,将会带回来几个响应头。而带回来的Cookie也在其中,以“响应头:key=value”的形式传回。在上图中,我们可以看到该请求带回了俩个Cookie

  • a=xxx
  • b=yyy

为啥有多个?

因为服务器的“记忆能力”实在是太差,一个Cookie可能不够用。所以,服务器有时会在响应头里添加多个 Set-Cookie,存储多个“key=value”。但浏览器这边发送时不需要用多个 Cookie 字段,只要在一行里用“;”隔开就行。

HTTP请求头:带上身份证

那么怎么解决每次手动加Cookie问题?这里就用到了HTTP的请求头Cookie,在后续的每次请求,浏览器都会将你接收的Cookie放到HTTP的请求头中,一起带到服务端。到现在,我们很清楚的知道Cookie是如何在浏览器与客户端流转了。

Cookie的过期时间

在我们的身份证上,都会带有过期时间,而对于Cookie而言,也会有他的过期时间。就像是食品的“保鲜期”,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。

Cookie的有效期可以使用两个属性字段来设置

  • “Expires” 俗称“过期时间”,用的是绝对时间点,可以理解为“截止日期”(deadline)。
  • “Max-Age” 用的是相对时间,单位是,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。

Expires 和 Max-Age 可以同时出现,两者的失效时间可以一致,也可以不一致,但浏览器会优先采用 Max-Age 计算失效期。

比如举个个例子里,Expires 标记的过期时间是“GMT 2023 年 11 月 1 号 零 点 过 20 秒”,而 Max-Age 则只有 10 秒,如果现在是 11 月 1 号零点,那么 Cookie 的实际有效期就是“11 月 1 号零点过 10 秒”。

当然,如果你没有设置Cookie的过期时间,那么他的生命周期就是浏览器关闭就没了。

Cookie 的作用域

中华人居民身份证在全中国都有效,但是一旦出了国外,就不一定有效了。而对于Cookie也同理,我们不能用A服务器的Cookie去访问B服务器,这是无效的。我们需要设置一下 Cookie 的作用域,让浏览器只发送给特定的服务器和 URL,避免被其他网站盗用。

Cookie的作用于可以使用下面两个属性字段设置

  • “Domain”:指定了Cookie所属的域名
  • “Path”:指定了 Cookie 所属路径

浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。

使用这两个属性可以为不同的域名和路径分别设置各自的 Cookie,比如“/avv-1”用一个 Cookie,“/avv-2”再用另外一个 Cookie,两者互不干扰。不过现实中为了省事,通常 Path 就用一个“/”或者直接省略,表示域名下的任意路径都允许使用 Cookie,让服务器自己去挑。

还有一点就是:

从上面那张图中我们也能够看到,Cookie 是由浏览器负责存储的,而不是操作系统。所以,它是“浏览器绑定”的,只能在本浏览器内生效。如果你换个浏览器或者换台电脑,新的浏览器里没有服务器对应的 Cookie,“健忘”的服务器也就认不出来了,只能再走一遍 Set-Cookie 流程了。

Cookie的安全性

写过前端的同学一定知道,在 JS 脚本里可以用 document.cookie 来读写 Cookie 数据,这就带来了安全隐患,有可能会导致“跨站脚本”(XSS)攻击窃取数据。

属性“HttpOnly”会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API,脚本攻击也就无从谈起了。

另一个属性“SameSite”可以防范“跨站请求伪造”(XSRF)攻击,设置成“SameSite=Strict”可以严格限定 Cookie 不能随着跳转链接跨站发送,而“SameSite=Lax”则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。

还有一个属性叫“Secure”,表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。

Chrome 开发者工具是查看 Cookie 的有力工具,在“Network-Cookies”里可以看到单个页面 Cookie 的各种属性,另一个“Application”面板里则能够方便地看到全站的所有 Cookie。

看完这篇,吃透Cookie和Session_第2张图片

Cookie常用来做什么
  • 广告跟踪
  • 自动登录

不过,我在访问一些网站的时候,他会问我需不需要接收该网站所有Cookie以便获取“更好”的服务。

好了,到此为止Cookie我们已经讲完了。下面以简单的登录方案来讲解Session。

登录方案:Cookie-Session

我们可以想一想,我们在使用任意一个网站的时候,是怎么一个流程。我们进去第一时间要么直接要求我们登录,要么等我们进去想去进行一些操作例如购物、评论这些操作的时候,就会被拉去强制登录。

那么他前端可能只存储了一个id字段,来查看你是否已经登录。我们来看第一种登录方案。

看完这篇,吃透Cookie和Session_第3张图片

  • 用户使用浏览器访问登录页面,输入账号密码。
  • 服务端收到请求,进行鉴权。
  • 成功后,将登录用户信息存储在Session当中,并且生成一个名为JSESSIONID 的 Cookie返回
  • 浏览器将JSEESIONID 保存在浏览器当中
  • 此后该浏览器再去请求业务接口,JSESSIONID 随 cookie 带上
  • 服务端查 JSESSIONID 校验 session
  • 成功后正常做业务处理,返回结果

这是后端使用Tomcat的Session所生成的JSESSIONID

伪代码:

public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {
	// 1. 校验
	// 2. 加密
	// 3. 记录用户的登录态
	request.getSession().setAttribute(USER_LOGIN_STATE, user);
	// return 
}
Session 的存储方式

Session被称为服务端存储数据技术,他依赖的仅仅是返回给前端的Cookie:SessionId,因此在服务端需要存储一些额外的数据,例如用户的个人信息。

存储的方式有几种:

  • Redis:内存型数据库。以 key-value 的形式存,正合 sessionId-sessionData 的场景;且访问快。
  • 内存:直接放到变量里。一旦服务重启数据就会消失(默认)
  • 数据库:普通数据库。性能不高。
Session 的过期和销毁(退出登录)

在不同的环境Session的设置是不一样的。在SpringBoot 2.7中是这样配置的。单位是秒(s)

spring:
  session:
    # 30 天过期
    timeout: 2592000

伪代码:

public boolean userLogout(HttpServletRequest request) {
	if (request.getSession().getAttribute(USER_LOGIN_STATE) == null) {
		throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录");
	}
	// 移除登录态
	request.getSession().removeAttribute(USER_LOGIN_STATE);
	return true;
}
布式下Session不共享问题

在分布式下,使用内存实现的Session则限制十分大,用户服务可能部署的不止一台机器上。所以用户请求过来会走一次负载均衡,不一定打到哪台机器上。那一旦用户后续接口请求到的机器和他登录请求的机器不一致,或者登录请求的机器宕机了,Session就会失效。

解决方案:

一是从存储角度,把 session 集中存储。如果我们用独立的 Redis 或普通数据库,就可以把 session 都存到一个库里。

二是从分布角度,让相同 IP 的请求在负载均衡时都打到同一台机器上。

但通常还是采用第一种方式,因为第二种相当于阉割了负载均衡模式,且仍没有解决用户请求的机器宕机的问题。

一些成熟的方案:

  • Tomcat配置Session共享(Session复制)
  • 引入Spring Session (第三方存储如存储在Redis)

这些方案不是重点,如果感兴趣可以去搜一搜。

你可能感兴趣的:(html5,javascript,java,vue.js)