看完这篇,吃透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的工作过程。

  • 浏览器向服务端的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也同理,我们不能用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常用来做什么

  • 广告跟踪
  • 自动登录

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

好了,到此为止开胃菜已经上齐了。我们开始今天的主题:如何实现登录?登录的方案背后的原理是什么

登录方案:Cookie-Session

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

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

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

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

伪代码:

java 复制代码
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)

yml 复制代码
spring:
  session:
    # 30 天过期
    timeout: 2592000

伪代码:

java 复制代码
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)

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

相关推荐
Asort17 小时前
JavaScript 从零开始(六):控制流语句详解——让代码拥有决策与重复能力
前端·javascript
无双_Joney17 小时前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥17 小时前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare17 小时前
选择文件夹路径
前端
艾小码17 小时前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月17 小时前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁17 小时前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅17 小时前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸17 小时前
Prompt结构化输出:从入门到精通的系统指南
前端