# 目录
# 前端鉴权由来
HTTP是无状态的,在HTTP请求方和响应方之前并不能做 状态维护 ,在 三次握手 四次挥手 之后,他们的状态都无法直接获取。比如 各类网站,都要求用户 登陆注册 ,这个时候HTTP该如何知道用户的 登录/等出状态 呢? 下文将例举利用 不同存储 作为一种 标记 来保存状态 ,实现 前端鉴权。
# 存储方案
- 挂载在全局变量上,但缺点是: 一刷新页面,数据就没了
- 挂载在 cookie、sessionStorage、localStorage等,不用担心刷新丢失的问题,开发者可以自由选择其中一种方式。
只要有地方存储,请求的时候 携参 给接口即可。
# Cookie存储
cookie 也是前端存储的一种,相比于 localStorage 等其他方式,借助 HTTP 头、浏览器能力,cookie 可以做到前端无感知。
一般过程是这样的:
- 在提供标记的接口,通过 HTTP 返回头的 Set-Cookie 字段,直接种到浏览器上
- 浏览器发起请求时,会自动把 cookie 通过 HTTP 请求头的 Cookie 字段,带给接口
# 配置: Domain/Path
可以通过 Domian
/Path
两级 去限制 空间范围
TIP
Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如 www.example.com (opens new window) 会设为 example.com,而且以后如果访问example.com的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。
Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是域名必须一致。
# 配置: Exipres/ Max-Age
可以通过 Expires
和Max-Age
中的一种,来限制cookie
的 时间范围
TIP
Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式。如果不设置该属性,或者设为null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。
Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。
如果同时指定了Expires和Max-Age,那么Max-Age的值将优先生效。
如果Set-Cookie字段没有指定Expires或Max-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。
# 配置: Secure / HttpOnly
用于配置cookie
的使用方式。
TIP
Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。
HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是Document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。 —— Cookie — JavaScript 标准参考教程(alpha)
# HTTP 头对 cookie 的读写
HTTP 返回的一个Set-Cookie
头用于向浏览器写入 一条 cookie,格式为 cookie 键值+配置键值。例如:
Set-Cookie: username=jimu; domain=jimu.com; path=/blog; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
若想多次set-cookie
,则多给几个即可(一次HTTP请求允许重复)
Set-Cookie: username=jimu; domain=jimu.com
Set-Cookie: height=180; domain=me.jimu.com
Set-Cookie: weight=80; domain=me.jimu.com
HTTP请求的Cookie
头用于浏览器把符合当前 空间、时间、使用方式 配置的所有cookie一并发给服务端。因为由浏览器做了筛选判断,就不需要归还配置内容了,只要发送键值就可以。
Cookie: username=jimu; height=180; weight=80
# 前端对 cookie的读写
前端可以自己创建 cookie
, 如果服务端创建的cookie
没有加上HttpOnly
, 那么前端也可以也可以修改他给的 cookie
。
调用document.cookie
可以创建、修改cookie
。和 HTTP
一样,能读到所有的 非HttpOnly
的 cookie
。
console.log(document.cookie);
// username=jimu; height=180; weight=80
TIP
在了解了 cookie
后,便知道cookie
是最便捷的维持HTTP请求状态的方式,大多数前端鉴权问题都是靠cookie解决的。当然也可以选用别的存储方式...
# 应用方案
# 服务端session
# 示例图

浏览器登录发送账号密码,服务端查用户库,校验用户
服务端把用户登录状态存为
Session
,生成一个sessionId
通过登录接口返回,把
sessionId
set
到cookie
上此后浏览器再请求业务接口,
sessionId
随cookie
带上服务端查
sessionId
校验session
成功后正常做业务处理,返回结果
# Session的存储方式
显然,服务端只是给cookie
一个sessionId
,而 session
的具体内容(可能包含用户信息、session 状态等)。存储的方式有几种:
- Redis(推荐):内存型数据库,redis中文官方网站 (opens new window)。以 key-value 的形式存,正合 sessionId-sessionData 的场景;且访问快。
- 内存:直接放到变量里。一旦服务重启就没了
- 数据库:普通数据库。性能不高。
# Session 的过期和销毁
只要把存储的 session 数据销毁就可以。
# Session 的分布式问题
通常服务端是集群,而用户请求过来会走一次负载均衡,不一定打到哪台机器上。那一旦用户后续接口请求到的机器和他登录请求的机器不一致,或者登录请求的机器宕机了,session 不就失效了吗?
这个问题现在有几种解决方式。
- 一是从存储角度,把 session 集中存储。如果我们用独立的 Redis 或普通数据库,就可以把 session 都存到一个库里。
- 二是从分布角度,让相同 IP 的请求在负载均衡时都打到同一台机器上。以 nginx 为例,可以配置
ip_hash
来实现。
但通常还是采用第一种方式,因为第二种相当于阉割了负载均衡,且仍没有解决用户请求的机器宕机的问题。
# node.js下的session处理
前面的图很清楚了,服务端要实现对cookie
和session
的存取,实现起来要做的事还是很多的。在npm
中,已经有封装好的中间件,比如express-session-npm
。
TIP
express-session - npm (opens new window) 主要实现了:
- 封装了对
cookie
的读写操作,并提供配置项配置字段、加密方式、过期时间等。 - 封装了对
session
的存取操作,并提供配置项配置session
存储方式(内存/redis)、存储规则等。 - 给req提供了
session
属性,控制属性的set/get
并响应到cookie
和session
存取上,并给req.session
提供了一些方法.
# 问题的萌生
session的维护给服务端造成很大困扰,我们必须找地方存放它,又要考虑分布式的问题,这似乎有些麻烦。
一个登录场景,要做的事情仅仅是:
- 登录,post 账号密码
- 校验成功,返回一个标记
- 请求接口携带标记
- 标记的校验
- 校验成功/失败,结果处理。
# 问题的解决
使用token
来做前端鉴权
# 应用方案: token
一个登录场景,也不必往 session 存太多东西,那为什么不直接打包到 cookie 中呢?这样服务端不用存了,每次只要核验 cookie 带的证件有效性就可以了,也可以携带一些轻量的信息。这种方式通常被叫做 token。
# token流程图

# token流程
token 的流程是这样的:
- 用户登录,服务端校验账号密码,获得用户信息
- 把 用户信息、token 配置编码成
token
,通过cookie
set
到浏览器 - 此后用户请求业务接口,通过 cookie 携带 token
- 接口校验 token 有效性,进行正常业务接口处理
# 客户端token的存储方式
cookie并不是客户端存储凭证的唯一方式。token 因为它的无状态性,有效期、使用限制都包在 token 内容里,对 cookie 的管理能力依赖较小,客户端存起来就显得更自由。但 web 应用的主流方式仍是放在 cookie 里。
# token的编码
通常都是采用base64
,比如 eyJ1c2VyaWQiOiJhIn0=
就是 {"userid":"abb”}
的转码结果。
# 防篡改
如果用户 cdd 拿{"userid":"abb”}
转了个 base64,再手动修改了自己的 token 为 eyJ1c2VyaWQiOiJhIn0=
,便能能直接发送请求访问到 abb 的数据。
# 解决方案
给token加密作为 签名 ,用来识别token是否被篡改过。例如在 cookie-session-npm
库中,增加了两项配置:
secret: 'iAmSecret',
signed: true,
这样会多种一个 .sig cookie, 里面的值就是 {"userid":"abb”}
和 iAmSecret
通过加密算法计算出来的。常见加密的链接: (HMACSHA256 类 (System.Security.Cryptography) | Microsoft Docs (opens new window)
# 示例图

现在cdd虽然造得出 abb
的token,但他无法造出加密后的签名,因此解决了 篡改 盗用他人token
的问题。
# 应用方案 JWT
上述token做法额外增加了 cookie 数量,数据本身也没有规范的格式,所以 JSON Web Token Introduction - jwt.io (opens new window) 横空出世了。
TIP
JSON Web Token (JWT) 是一个开放标准,定义了一种传递 JSON 信息的方式。这些信息通过数字签名确保可信。
它是一种成熟的 token 字符串生成方案,包含了我们前面提到的数据、签名。代码示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJhIiwiaWF0IjoxNTUxOTUxOTk4fQ.2jf3kl_uKWRkwjOP6uQRJFqMlwSABcgqqcJofFH5XCo
# 示例图

类型、加密算法的选项,以及 JWT 标准数据字段,可以参考 RFC 7519 - JSON Web Token (JWT) (opens new window)
node 上同样有相关的库实现:express-jwt - npm (opens new window) koa-jwt - npm (opens new window)
# refresh token
业务接口用来鉴权的 token,我们称之为 access token。越是权限敏感的业务,我们越希望 access token 有效期足够短,以避免被盗用。但过短的有效期会造成 access token 经常过期,过期后怎么办呢?
一种办法是,让用户重新登录获取新 token,显然不够友好,要知道有的 access token 过期时间可能只有几分钟。
另外一种办法是,再来一个 token,一个专门生成 access token 的 token,我们称为 refresh token。
- access token 用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活
- refresh token 用来获取 access token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,也可以如前面的 session 一样处理
有了 refresh token 后,几种情况的请求流程变成这样:
# 示例图

# session和token
session 和 token 都是边界很模糊的概念,就像前面说的,refresh token 也可能以 session 的形式组织维护。
狭义上,我们通常认为 session 是种在 cookie 上、数据存在服务端的认证方案,token 是客户端存哪都行、数据存在 token 里的认证方案。对 session 和 token 的对比本质上是客户端存 cookie / 存别地儿、服务端存数据 / 不存数据的对比。
客户端存 cookie / 存别地儿
存 cookie 固然方便不操心,但问题也很明显:
- 在浏览器端,可以用 cookie(实际上 token 就常用 cookie),但出了浏览器端,没有 cookie 怎么办?
- cookie 是浏览器在域下自动携带的,这就容易引发 CSRF 攻击(前端安全系列(二):如何防止CSRF攻击? - 美团技术团队 (opens new window))
存别的地方(例如http头部Authorization: Bearer <token>
等),可以解决没有 cookie 的场景;通过参数等方式手动带,可以避免 CSRF
攻击。
服务端存数据 / 不存数据
- 存数据:请求只需携带 id,可以大幅缩短认证字符串长度,减小请求体积
- 不存数据:1. 不需要服务端整套的解决方案和分布式处理,降低硬件成本 2. 避免查库带来的验证延迟
# 总结
- HTTP 是无状态的,为了维持前后请求,需要前端存储标记
- cookie 是一种完善的标记方式,通过 HTTP 头或 js 操作,有对应的安全策略,是大多数状态管理方案的基石
- session 是一种状态管理方案,前端通过 cookie 存储 id,后端存储数据,但后端要处理分布式问题
- token 是另一种状态管理方案,相比于 session 不需要后端存储,数据全部存在前端,解放后端,释放灵活性
- token 的编码技术,通常基于 base64,或增加加密算法防篡改,jwt 是一种成熟的编码方案
- 在复杂系统中,token 可通过 service token、refresh token 的分权,同时满足安全性和用户体验
- session 和 token 的对比就是用不用cookie和后端存不存的对比
# 参考文章
← 前端如何处理10万条数据 各作用域的理解 →