从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 的作用域
中华人居民身份证在全中国都有效,但是一旦出了国外,就不一定有效了。而对于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)
这些方案不是重点,如果感兴趣可以去搜一搜。