1 概述:无状态的困境与身份认证的诞生
HTTP 协议的核心特征之一是无状态。这意味着服务器不会“记住”它处理过的每一个请求。每一次客户端(如浏览器)向服务器发起请求,都被视为一次全新的、独立的对话。服务器处理完请求后,连接就关闭,不会保留任何关于这次请求或之前请求的上下文信息。
这种设计简单高效,但也带来了一个核心问题:如何关联多次请求? 例如,用户登录成功后,在后续访问个人主页或购物车页面时,服务器如何确认这些请求都是同一个合法用户发出的?服务器需要一种方法来识别“谁”在请求。
为了解决这个身份认证与状态保持的难题,Cookie、Session 和 Token 应运而生。它们本质上都是为客户端(或代表用户的请求)提供一种身份凭证。这就像给每个登录用户或设备颁发了一张“身份证”,服务器通过验证这张“身份证”,就能识别请求来源,进而实现访问控制(权限管理)和个性化服务。
2 深入剖析:Cookie
2.1 概念与定位
Cookie 是由服务器生成并发送给客户端(通常是浏览器)保存的一小块文本数据。当客户端再次向同一服务器发起请求时,浏览器会自动将与该服务器关联的 Cookie 附加在请求头中发送回去。服务器通过解析这些 Cookie,就能获取之前存储的状态信息(如用户标识、偏好设置等),从而实现状态的保持。
Cookie 主要存储在客户端浏览器端,有大小限制(通常约 4KB)。虽然早期曾被广泛用于存储各种客户端数据,但随着 Web Storage (LocalStorage, SessionStorage) 等更优方案的普及,Cookie 在现代 Web 开发中主要专注于其核心职责:在客户端可靠地存储和传递身份标识或会话信息,以支持身份认证和状态管理。
2.2 主要属性详解
Cookie 的配置依赖于其属性,这些属性共同决定了 Cookie 的行为:
属性名 | 描述 |
---|---|
name | Cookie 的唯一标识名称。 |
value | Cookie 存储的实际值(字符串形式)。 |
domain | 指定 Cookie 有效的域名范围。例如,设置为 .example.com ,则 a.example.com 和 b.example.com 都可访问该 Cookie。 |
path | 指定 Cookie 在域名下的有效路径。只有请求路径匹配该路径或其子路径时,才会携带该 Cookie。默认为 / 。 |
expires | 指定一个具体的 GMT/UTC 时间作为 Cookie 的过期时间。过期后浏览器将自动删除该 Cookie。 |
maxAge | 指定 Cookie 从现在起存活的秒数。优先级高于 expires 。设置为 0 表示立即删除,为负数表示会话结束(关闭浏览器)时删除。 |
secure | 布尔值。若为 true ,则 Cookie 仅在通过 HTTPS 等安全加密连接时才会被发送。 |
httpOnly | 布尔值。若为 true ,则该 Cookie 无法通过客户端的 JavaScript 脚本(如 document.cookie )读取或修改。这能有效降低 XSS(跨站脚本攻击)窃取敏感 Cookie 的风险。 |
sameSite | 控制浏览器是否在跨站请求中发送 Cookie。可选值:Strict (严格禁止跨站发送)、Lax (宽松,允许部分安全跨站请求如导航链接)、None (允许跨站发送,但必须同时设置 Secure )。现代浏览器默认常为 Lax 。 |
2.3 核心工作流程(结合 SessionId)
Cookie 最常见的用途是存储 Session ID,实现基本的会话管理。其流程如下:
-
登录请求: 用户提交登录信息(如用户名密码)到服务器。
-
会话创建: 服务器验证凭据通过后,在服务器端创建一个新的会话(Session),其中存储了与该用户相关的状态信息(如用户ID、登录状态)。服务器为该会话生成一个全局唯一的标识符:Session ID。
-
设置 Cookie: 服务器在响应头中添加
Set-Cookie
指令,将 Session ID 作为 Cookie 的值发送给客户端。例如:Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure
。 -
客户端存储: 浏览器接收到响应后,会按照指令将包含 Session ID 的 Cookie 安全地(如果设置了
HttpOnly
和Secure
)存储在本地。 -
后续请求: 当用户访问该网站的其他页面时,浏览器会自动在请求头中携带与该域名和路径匹配的 Cookie(即包含
sessionId=abc123
的 Cookie)。 -
会话识别: 服务器收到请求后,从请求头的 Cookie 中提取 Session ID。然后根据这个 ID 去查找服务器端存储的对应 Session 对象。
-
请求处理: 如果找到有效的 Session,服务器就知道当前请求来自已登录的用户,并可以使用 Session 中存储的信息处理请求(如显示用户名)。如果 Session 无效或过期,服务器会要求用户重新登录。
2.4 关键特性总结
-
客户端存储: 数据存储在浏览器端。
-
自动携带: 满足域名、路径、安全条件时,浏览器自动在请求中附加 Cookie。
-
大小限制: 约 4KB,且单个域名下 Cookie 数量也有限制。
-
安全依赖: 敏感 Cookie(如 Session ID)应始终设置
Secure
(仅 HTTPS)和HttpOnly
(防 XSS)属性。SameSite
属性是防御 CSRF(跨站请求伪造)攻击的重要手段。 -
域限制: Cookie 默认不可跨域,但通过设置
domain
属性可在一级域名及其子域名间共享。
3 深入剖析:Session
3.1 概念与定位
Session 代表的是服务器端为特定用户(客户端)创建的一次交互会话上下文。当用户首次访问服务器或完成登录时,服务器会为其创建一个唯一的 Session 对象。这个对象存储在服务器的内存、文件系统、数据库或缓存(如 Redis)中。
Session 的核心作用是在服务器端安全地存储和管理用户的状态信息(如用户ID、登录状态、购物车内容、临时数据等)。与 Cookie 存储少量标识信息不同,Session 可以存储更丰富、更敏感的数据,且数据本身不直接暴露给客户端。
3.2 Session ID 的传递与关联
Session 本身存在于服务器端,客户端如何与它关联呢?答案就是 Session ID。服务器创建 Session 后,会生成一个唯一的 Session ID,并通过 Set-Cookie
指令(如 sessionId=abc123
)发送给客户端浏览器存储(即上文 Cookie 流程中的第 2、3 步)。后续请求中,客户端浏览器通过 Cookie 将这个 Session ID 带回给服务器(第 5 步)。服务器收到请求后,解析出 Session ID(第 6 步),并用它来查找对应的服务器端 Session 对象(第 7 步)。因此,Session ID 是连接客户端 Cookie 与服务器端 Session 对象的桥梁。
3.3 Session 工作流程图
3.4 与 Cookie 的核心区别与联系
-
存储位置:
-
Cookie:数据存储在客户端(浏览器)。
-
Session:数据存储在服务器端。
-
-
安全性:
-
Session:更安全。敏感数据(如用户身份、权限)存储在服务器,客户端只持有无意义的 Session ID(仍需妥善保护该 ID)。
-
Cookie:存储在客户端,存在被窃取(XSS)或篡改的风险。敏感信息不应直接存储在 Cookie 中。
-
-
存储内容与容量:
-
Cookie:只能存储字符串,容量小(~4KB),受浏览器限制。
-
Session:可存储任意类型数据(对象、数组等),容量理论上只受服务器资源限制。
-
-
生命周期:
-
Cookie:可通过
expires
/maxAge
设置较长的有效期(如记住我功能)。 -
Session:通常有效期较短(如用户不活动 30 分钟后过期),依赖于 Session ID 的传递(若 Cookie 失效或用户清除 Cookie,Session 即失效)。服务器端也可主动清理 Session。
-
-
依赖关系:
-
Session 的识别通常依赖于 Cookie 来传递 Session ID。虽然也可以通过 URL 重写等方式传递,但 Cookie 是最主流、最方便的方式。
-
4 深入剖析:Token
4.1 概念与定位
Token(令牌)是一种自包含的、用于在客户端和服务端之间安全传递身份和授权信息的凭证。与 Session 不同,Token 的设计理念是无状态(Stateless):服务器不需要在本地存储会话信息。用户登录成功后,服务器生成一个 Token 并发送给客户端。客户端在后续请求中携带此 Token(通常在 Authorization
请求头中,如 Bearer <token>
)。服务器只需验证 Token 本身的有效性(签名是否合法、是否过期等)和其中包含的信息(如用户ID、权限),即可完成身份认证和授权,无需去查询服务器端的会话存储。
Token 是解决分布式系统、跨域认证(如单点登录 SSO)、移动应用 API 认证等场景下状态管理问题的理想方案。常见的 Token 标准是 JWT (JSON Web Token)。
4.2 Token 的核心组成(以 JWT 为例)
一个典型的 Token (JWT) 由三部分组成,用点 (.
) 分隔:
-
Header (标头): 通常包含 Token 的类型(如
JWT
)和签名算法(如HS256
,RS256
)。例如:{"alg": "HS256", "typ": "JWT"}
。此部分会进行 Base64Url 编码。 -
Payload (有效载荷): 包含需要传递的声明(Claims)。声明是关于实体(通常是用户)和附加数据的语句。常见声明包括:
-
注册声明 (Registered Claims): 预定义的标准字段(非强制),如
iss
(签发者),exp
(过期时间),sub
(主题/用户ID),aud
(受众) 等。 -
公共声明 (Public Claims): 可自定义的、公开的字段。
-
私有声明 (Private Claims): 自定义的、在通信双方之间共享的字段(如
userId
,role
)。此部分也会进行 Base64Url 编码。注意:Payload 默认是可见的(Base64Url 可解码),因此不应在 Payload 中存储敏感信息(如密码)。如需存储敏感信息,必须对 Token 整体进行加密(JWE)。
-
-
Signature (签名): 将编码后的 Header、编码后的 Payload 和一个密钥(Secret,只有服务器知道)通过 Header 中指定的签名算法(如 HMAC SHA256)计算生成。签名用于验证 Token 在传输过程中没有被篡改,并且证明该 Token 是由持有密钥的合法服务器签发的。
一个完整的 JWT 格式:xxxxx.yyyyy.zzzzz
(Header.Payload.Signature)。
4.3 Token 认证流程
-
登录请求: 客户端发送登录凭证(用户名/密码等)到认证服务器。
-
验证凭证 & 生成 Token: 服务器验证凭证有效后,使用预定义的密钥和算法(如 HS256)生成一个 JWT。Payload 中通常包含用户标识(如
sub: user123
)、过期时间(exp
)和其他必要信息。 -
返回 Token: 服务器将生成的 Token 返回给客户端(通常在响应体中)。
-
客户端存储 Token: 客户端(浏览器、移动App)安全地存储此 Token(如 Web Storage、安全 Cookie、移动端安全存储)。
-
携带 Token 请求: 客户端访问受保护的 API 或资源时,在请求头(通常是
Authorization: Bearer <token>
)或请求参数(较少推荐)中携带此 Token。 -
验证 Token: 资源服务器(或 API 网关)收到请求:
-
检查
Authorization
头是否存在且格式正确。 -
提取 Token。
-
验证签名: 使用相同的密钥和算法对 Header + Payload 重新计算签名,并与 Token 中的 Signature 部分进行比对,确保 Token 未被篡改。
-
验证声明: 检查
exp
确保 Token 未过期;检查iss
确认签发者可信;检查aud
确认 Token 是发给本服务的(可选)等。
-
-
处理请求: 如果 Token 验证通过,服务器解析 Payload(如获取
sub
中的用户ID),根据其中的信息进行授权(如检查用户是否有权限访问该资源)并处理请求。服务器在此过程中无需查询数据库或缓存中的会话状态(无状态)。如果验证失败,返回 401 Unauthorized 或 403 Forbidden 错误。