前言
在现在的互联网服务中,用户认证是至关重要的环节。其一般流程大致如下:
- 用户向服务器发送用户名和密码,以发起登录请求。
- 服务器对用户提供的信息进行验证。验证通过后,在当前会话(session)中保存相关数据,例如用户角色、登录时间等关键信息。
- 服务器向用户返回一个唯一的 session_id,并将其写入用户的 Cookie 中。
- 用户后续的每一次请求,都会通过 Cookie 将 session_id 传递回服务器。
- 服务器接收到 session_id 后,查找前期保存的数据,从而确定用户的身份,进而为用户提供相应的服务和权限。
然而,这种传统的认证模式在扩展性方面存在一定的局限性。在单机环境下,它运行良好,但一旦涉及服务器集群或跨域的服务导向架构,就需要解决 session 数据共享的问题,确保每台服务器都能够读取和使用 session 数据。
例如,假设 A 网站和 B 网站属于同一家公司的关联服务,我们期望用户在其中一个网站成功登录后,访问另一个网站时能够自动登录。那么,如何实现这一功能呢?
方案一:Session 数据持久化
一种可行的解决方案是将 session 数据持久化,例如将其写入数据库或其他持久存储层。这样,各种服务在收到请求后,都可以向持久层请求所需的数据。此方案的优点在于架构清晰,逻辑明确。但缺点也较为明显,其工程量相对较大。而且,如果持久层出现故障,将会导致单点失败,整个系统的稳定性将受到严重影响。
方案二:基于客户端存储的 JWT 方案
另一种创新的方案是服务器不再保存 session 数据,而是将所有与用户相关的数据都保存在客户端。每次请求时,客户端将这些数据发回服务器。JWT(JSON Web Token)就是这种方案的典型代表。
HTTP 的无状态特性
我们都清楚,HTTP 是一种无状态(stateless)的协议。这意味着 HTTP 对于事务处理没有记忆能力,不会保存请求和响应之间的通信状态。每次有新的请求发送时,都会产生相应的新响应。协议本身并不保留之前的请求或响应报文的任何信息。这种设计的初衷是为了能够快速处理大量事务,确保协议的可伸缩性,从而使 HTTP 协议保持简洁高效。
然而,随着 Web 应用的不断发展,这种无状态的特性在某些场景下带来了诸多不便。以用户登录新浪微博为例,用户在登录页输入用户名、密码后进入首页。但由于 HTTP 无状态的特性,HTTP 无法知晓上一次的请求是否通过了验证,更无法获取当前用户的具体信息。
最直接的解决办法或许是在所有的请求中都附带用户名和密码。这种方法虽然在技术上可行,但会极大地增加服务器的负担(因为服务器需要针对每个请求都到数据库进行验证),同时也会给用户带来极差的体验,因为用户需要在每进入一个页面时都输入密码。
为了解决这些问题,各种身份认证机制应运而生,其中较为常见的有 Cookie-Session 机制和 JWT 机制。
Cookie-Session 机制
Cookie 简介
Cookie 是由 HTTP 服务器设置,并保存在浏览器中的小型文本文件,其内容以一系列的键值对形式呈现。在 Chrome 浏览器中,您可以通过开发者工具 -> Application -> Cookies 查看 Cookie 的详细信息。
以下简单介绍一些常见的 Cookie 字段含义:
Expires
:指定 Cookie 的过期时间。默认情况下,Cookie 在用户关闭浏览器时过期。HttpOnly
:此属性指示浏览器不要在除了 HTTP(或者 HTTPS)请求之外暴露 Cookie。通过 JavaScript 脚本无法访问到具有HttpOnly
属性的 Cookie,这能有效防止 XSS(跨站脚本攻击)。Secure
:当设置 Cookie 的Secure
属性为true
时,意味着 Cookie 仅能在安全/加密连接(如 HTTPS)下使用。也就是说,只有在 HTTPS 协议下,Cookie 才能被上传到服务器,而在 HTTP 协议下无法上传。
Cookie 的传递过程
- 浏览器向某个 URL 发送请求。
- 对应的服务器收到该 HTTP 请求后,生成要发送给浏览器的 HTTP 响应。
- 在响应头中添加
Set-Cookie
字段,其值为要设置的 Cookie 内容。 - 浏览器收到来自服务器的 HTTP 响应。
- 浏览器在响应头中发现了
Set-Cookie
字段,便会将该字段的值保存在内存或硬盘中。 - 当下一次向该服务器发送 HTTP 请求时,会将服务器设置的 Cookie 附加在 HTTP 请求的
Cookie
字段中。 - 服务器收到这个 HTTP 请求后,若发现请求头中有
Cookie
字段,就知道已经处理过这个用户的请求了。 - 过期的 Cookie 会被浏览器自动删除。
Session 介绍
与存储在浏览器中的 Cookie 不同,Session 数据是存储在服务器端的,这样可以避免在客户端存储敏感信息。而且,Session 的存取方式更为灵活,能够存储任何类型的数据,而 Cookie 只能保存 ASCII 字符串,如果需要存取 Unicode 字符或二进制数据,则需要先进行编码。通常,Session 会与 Cookie 配合使用,这就是接下来要探讨的 Cookie-Session 机制。
Cookie-Session 身份验证机制的工作流程
- 用户输入登录信息。
- 服务端验证登录信息的正确性。如果验证通过,就在服务器端为该用户创建一个 Session,并将 Session 数据存入数据库。
- 服务器端向客户端返回带有 sessionID 的 Cookie。
- 客户端接收到服务器端发来的请求后,在响应头中看到
Set-Cookie
字段,将 Cookie 保存起来。 - 在接下来的请求中,客户端都会带上这个 Cookie。服务器将收到的 sessionID 与数据库中的进行匹配,如果匹配有效,则处理该请求。
- 如果用户登出,Session 会在客户端和服务器端都被销毁。
Cookie-Session 机制的缺陷
- 扩展性不佳:在多服务器的环境下,如何共享 Session 数据成为难题。例如,当用户首次访问的是服务器 A,而第二次请求被转发到服务器 B 时,服务器 B 无法获取用户的状态信息。
- 安全性欠佳:攻击者可能利用本地 Cookie 进行欺骗和 CSRF(跨站请求伪造)攻击。
- 对服务器性能有影响:由于 Session 数据保存在服务器端,如果短时间内有大量用户登录,会占用大量服务器内存,影响服务器性能。
- 跨域问题:Cookie 受到同源策略的限制。
JWT 机制
JWT 的组成
JWT 由三个部分组成:header(头部)、payload(负载)和 signature(签名),每个部分之间使用 .
分隔。其中,header 和 payload 使用 Base64URL 进行编码:
base64UrlEncode(header).base64UrlEncode(payload).signature
Header(头部)
header 部分是一个 JSON 对象,用于描述 JWT 的元数据,例如:
{
"typ": "JWT",
"alg": "HS256"
}
其中,typ
表示这是一个 JWT 对象,alg
表示用于创建签名的 Hash 算法,这里使用的是 HMAC-SHA256 算法。
Payload(负载)
payload 部分同样是一个 JSON 对象,实际需要传递的数据就存放在这里。除了使用官方提供的七个字段之外,还可以自定义私有字段,例如:
{
"sub": "title",
"name": "Yeoman"
}
需要注意的是,JWT 默认是不加密的,任何人都可以读取其中的内容,因此不要在 payload 中存放敏感信息。
Signature(签名)
signature 是对前两个部分的签名,用于防止数据被篡改。其生成过程如下:
data = base64urlEncode( header ) + "." + base64urlEncode( payload );
signature = Hash( data, secret );
首先,将使用 Base64URL 编码的 header 和 payload 用 .
连接起来,然后使用 header 中指定的 Hash 算法(如上述的 HMAC-SHA256),结合一个密钥对这个字符串进行 Hash 运算,得到 signature。
JWT 的工作流程
- 前端将自己的用户名和密码发送到后端的接口。
- 后端核对用户名和密码。如果正确,将用户的一些信息作为 payload,生成 JWT。
- 后端将 JWT 作为登录成功的返回结果返回给前端。前端可以将其结果保存在 localStorage 或 sessionStorage 中,登出时删除 JWT 即可。(建议不要保存在 Cookie 中,因为使用 Cookie 无法设置
HttpOnly
属性,且存在跨域问题。) - 每一次请求都将 JWT 放在 HTTP 请求头中的
Authorization
字段中,格式为Authorization: Bearer <token>
,这样相比放在 Cookie 中能够实现跨域请求。 - 服务器接收到请求后,对 JWT 进行解码。如果 token 有效,则处理该请求。
- 用户登出时,在客户端删除 token 即可,与服务端无关。
JWT 的特点
- JWT 默认是不加密的,因此其安全性相对较低。
- JWT 的主要目的是验证来源的可靠性,而不是保护数据或防止未经授权的访问。可以将其类比为一张电影票,它只能验证电影票的真伪以及包含一些基本信息,但他人也可能使用您的电影票。一旦 JWT 被暴露,任何人都可能获得相应的权限。为了降低被盗用的风险,JWT 的有效期应设置得相对较短。对于一些重要的权限,使用时应再次对用户进行认证。
- JWT 最大的缺点是 token 过期处理问题。由于服务器不保存 Session 状态,因此无法在使用过程中废止或更改权限。也就是说,一旦 JWT 签发,在到期之前它始终有效,除非服务器部署额外的逻辑来处理。
几个case
同源策略限制的内容
同源策略限制的内容包括:Cookie、LocalStorage、SessionStorage、IndexedDB 等存储性内容;DOM 节点;Ajax 发送请求后,结果被浏览器拦截。
Cookie 和 Session 的区别
- 存取方式:Cookie 只能保存 ASCII 字符串,如需存取 Unicode 字符或二进制数据,需先进行编码。Session 则可以存取任何类型的数据。
- 隐私策略:Cookie 存储在浏览器中,Session 存储在服务器上。
- 服务器压力:Session 保存在服务器上,每个用户都会产生一个 Session。在并发访问用户众多的情况下,会产生大量的 Session,消耗大量服务器内存。
分布式情况下的 Session 和 Token
我们已经了解到 Session 是有状态的,通常存储于服务器的内存或硬盘中。当服务器采用分布式或集群架构时,Session 就会面临负载均衡的问题。
在大型互联网公司中,为了支撑巨大的流量,后端往往需要多台服务器共同来处理前端用户的请求。如果用户在 A 服务器登录,第二次请求却被分发到服务 B,就可能出现登录失效的问题。
针对分布式 Session,一般有以下几种解决方案:
- Nginx 的
ip_hash
策略:服务端使用 Nginx 作为代理,每个请求按照访问 IP 的哈希值进行分配。这样,来自同一 IP 的请求会固定访问一个后台服务器,避免了在服务器 A 创建 Session,第二次请求却分发到服务器 B 的情况。 - Session 复制:任何一个服务器上的 Session 发生变更(如增加、删除、修改),该节点会将这个 Session 的所有内容序列化,然后广播给其他所有节点。
- 共享 Session:将服务端设置为无状态,使用缓存中间件来统一管理用户的 Session 等信息,确保分发到每一个服务器的响应结果都一致。
综合考虑,建议采用第三种方案。
而 Token 是无状态的,Token 字符串中保存了所有的用户信息。客户端登录时向服务端传递信息,服务端接收后将用户信息加密生成 Token 并返回给客户端。客户端将 Token 存放在 localStorage 等容器中。客户端每次访问时都传递 Token,服务端对 Token 进行解密,从而确定用户身份。通过这种 CPU 加解密的方式,服务端无需存储 Session,节省了存储空间,有效地解决了负载均衡和多服务器环境下的问题。这种方法也就是 [JWT(Json Web Token)]