Cookie、Session、Token 与 JWT

Cookie、Session、Token 与 JWT

最后更新于 2020-04-12

最近在复习相关内容,打算从头的整理一下 CookieSessionTokenJWT 相关内容,彻底弄清它们的含义以及它们之间的区别,主要内容包括以下这些

  • 认证(Authentication
  • 授权(Authorization
  • 凭证(Credentials
  • 什么是 Cookie
  • 什么是 Session
  • 什么是 Token
  • 什么是 JWT
  • CookieSession 的区别
  • TokenSession 的区别
  • TokenJWT 的区别
  • 常见的加密算法
  • 常见问题

下面就让我们一个一个来进行了解,先从几个基本概念开始看起

认证(Authentication)

关于认证,通俗地讲就是验证当前用户的身份,证明你是你自己(比如你每天上下班打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功),而互联网中的认证则包括

  • 用户名密码登录
  • 邮箱发送登录链接
  • 手机号接收验证码
  • 只要你能收到邮箱/验证码,就默认你是账号的主人

授权(Authorization)

关于授权,简单来说就是用户授予第三方应用访问该用户某些资源的权限,比如你在安装手机应用的时候,App 会询问是否允许授予权限(访问相册、地理位置等权限),又或者你在访问微信小程序时,在登录的时候,小程序会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息等),实现授权的方式一般有 Cookie、Session、Token、OAuth

凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份,在现实生活中,每个人都会有一张专属的居民身份证,是用于证明持有人身份的一种法定证件,通过身份证,我们可以办理手机卡/银行卡/个人贷款/交通出行等等,这就是认证的凭证

而在互联网应用中,一般网站会有两种模式,游客模式和登录模式,在游客模式下,你可以正常浏览网站上面的文章,一旦想要实现某些交互操作等(比如评论),就需要登录或者注册账号,当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(Token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌,就可以使用游客模式下无法使用的功能

在看完了几个基本概念以后,我们就正式的来了解一下什么是 Cookie,众所周知,HTTP 是一个无状态协议,所以客户端每次发出请求时,下一次请求无法得知上一次请求所包含的状态数据,那么我们如何能把一个用户的状态和数据关联起来呢?

比如在某个页面中,你进行了登录操作,而当你跳转到商品页时,服务端如何知道你是已经登录的状态呢?所以在这种情况下就产生了 Cookie 这门技术来解决这个问题,CookieHTTP 协议的一部分,它的处理分为如下几步

  1. 客户端发送 HTTP 请求到服务器
  2. 当服务器收到 HTTP 请求时,在响应头里面添加一个 Set-Cookie 字段
  3. 浏览器收到响应后保存下 Cookie
  4. 之后对该服务器每一次请求中都通过 Cookie 字段将 Cookie 信息发送给服务器

这里有几个需要注意的地方,首先 Cookie 是存储在客户端的,其次 Cookie 是不可跨域的,每个 Cookie 都会绑定单一的域名,无法在别的域名下获取使用,但是一级域名和二级域名之间是允许共享使用的(依靠的是 domain

其次 Cookie 主要用于以下三个方面

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

下面我们来看看一些 Cookie 当中比较重要的参数

Name/Value

键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型(如果值为 Unicode 字符,需要为字符编码,如果值为二进制数据,则需要使用 BASE64 编码),所以我们在用 JavaScript 操作 Cookie 的时候需要注意对 Value 进行编码处理

Expires

Expires 用于设置 Cookie 的过期时间,比如

1
Set-Cookie: id=b7fGcj3fWa; Expires=Wed, 21 Oct 2017 07:28:00 GMT;

Expires 属性缺省时,表示是会话性 Cookie,当为会话性 Cookie 的时候,值保存在客户端内存中,并在用户关闭浏览器时失效,需要注意的是,有些浏览器提供了会话恢复功能,这种情况下即使关闭了浏览器,会话期 Cookie 也会被保留下来,就好像浏览器从来没有关闭一样

与会话性 Cookie 相对的是持久性 Cookie,持久性 Cookie 会保存在用户的硬盘中,直至过期或者清除 Cookie,这里值得注意的是,设定的日期和时间只与客户端相关,而不是服务端

Max-Age

Max-Age 用于设置在 Cookie 失效之前需要经过的秒数,比如

1
Set-Cookie: id=b7fGcj3fWa; Max-Age=604800

Max-Age 可以为正数、负数、甚至是 0,具体区别如下

  • 如果 max-Age 属性为正数时,浏览器会将其持久化,即写到对应的 Cookie 文件中
  • max-Age 属性为负数,则表示该 Cookie 只是一个会话性 Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该 Cookie
  • max-Age0 时,则会立即删除这个 Cookie

假如 ExpiresMax-Age 都存在,则 Max-Age 优先级更高

Domain

Domain 指定了 Cookie 可以送达的主机名,假如没有指定,那么默认值为当前文档访问地址中的主机部分(但是不包含子域名),比如某个站点首页设置的 Domain.test.com,这样无论是 a.test.com 还是 b.test.com 都可以使用 Cookie

但是这里需要注意的是,不能跨域设置 Cookie,比如 a 域名下的页面把 Domain 设置成 b 是无效的

1
Set-Cookie: qwerty=219ffwef9w0f; Domain=b.com; Path=/; Expires=Wed, 30 Aug 2017 00:00:00 GMT

Path

Path 指定了一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部,比如设置 Path=/docs/docs/Web/ 下的资源会带 Cookie 首部,/test 则不会携带 Cookie 首部

DomainPath 标识共同定义了 Cookie 的作用域,即 Cookie 应该发送给哪些 URL

Secure

标记为 SecureCookie 只应通过被 HTTPS 协议加密过的请求发送给服务端,默认为 false,当值为 true 时,CookieHTTP 中是无效,在 HTTPS 中才有效

使用 HTTPS 安全协议,可以保护 Cookie 在浏览器和 Web 服务器间的传输过程中不被窃取和篡改

HTTPOnly

如果给某个 Cookie 设置了 httpOnly 属性,则无法通过 JavaScript 脚本读取到该 Cookie 的信息,但还是能通过 Application 中手动修改 Cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全

SameSite

这里我们重点来看看这个属性,因为在二月份发布的 Chrome 80 的版本中已经默认屏蔽掉了第三方的 Cookie,这样一来就导致了许多问题,我们先来看看这个属性的作用,其实简单来说就是『SameSite 属性可以让 Cookie 在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)』

SameSite 可以有下面三种值

  • Strict,仅允许一方请求携带 Cookie,即浏览器将只发送相同站点请求的 Cookie,即当前网页 URL 与请求目标 URL 完全一致
  • Lax,允许部分第三方请求携带 Cookie
  • None,无论是否跨站都会发送 Cookie

之前默认是 None 的,而 Chrome 80 后默认是 Lax

跨域和跨站

首先要理解的一点就是『跨站』和『跨域』是不同的,同站(same-site)与跨站(cross-site)和第一方(first-party)与第三方(third-party)是等价的,但是与浏览器同源策略(SOP)中的同源(same-origin)与跨域(cross-origin)是完全不同的概念

同源策略作为浏览器的安全基石,其『同源』判断是比较严格的,相对而言,Cookie 中的『同站』判断就比较宽松,只要两个 URLeTLD + 1 相同即可,不需要考虑协议和端口,其中 eTLD 表示有效顶级域名,注册于 Mozilla 维护的公共后缀列表(Public Suffix List)中,例如 .com.co.uk.github.io 等,其中的 eTLD + 1 则表示,有效顶级域名加上二级域名,例如 test.com

举几个例子来说的话就是 www.taobao.comwww.baidu.com 是跨站,www.a.taobao.comwww.b.taobao.com 是同站,a.github.iob.github.io 是跨站(注意是跨站)

改变

接下来我们来看下从 None 改成 Lax 到底影响了哪些地方的 Cookies 的发送?见下表

请求类型 实例 以前 Strict Lax None
链接 <a href=""></a> 发送 Cookie 不发送 发送 Cookie 发送 Cookie
预加载 <link rel="prerender" href="" /> 发送 Cookie 不发送 发送 Cookie 发送 Cookie
GET 表单 <form method="GET" action=""> 发送 Cookie 不发送 发送 Cookie 发送 Cookie
POST 表单 <form method="POST" action=""> 发送 Cookie 不发送 不发送 发送 Cookie
iframe <iframe src=""></iframe> 发送 Cookie 不发送 不发送 发送 Cookie
Ajax $.get() 发送 Cookie 不发送 不发送 发送 Cookie
Image <img src="" /> 发送 Cookie 不发送 不发送 发送 Cookie

从上图可以看出,对大部分 Web 应用而言,POST 表单,iframeAjaxImage 这四种情况从以前的跨站会发送三方 Cookie,变成了不发送,主要原因如下

  • iframe 嵌入的 Web 应用有很多是跨站的,都会受到影响
  • Ajax 可能会影响部分前端取值的行为和结果
  • Image 一般都存放在 CDN 上,大部分情况不需要 Cookie,故影响有限,但如果引用了需要鉴权的图片,可能会受到影响
问题

我们再看看会出现什么的问题?举几个例子

  • 通过接口获取的登录信息,由于 Cookie 丢失,用户无法登录,页面还会误判断成是由于用户开启了浏览器的禁止第三方 Cookie 功能导致而给与错误的提示
  • 一些站点上使用 iframe 嵌入的部分,没有了 Cookie,都会受到影响
  • 一些埋点系统会把用户 id 信息埋到 Cookie 中,用于日志上报,这种系统一般走的都是单独的域名,与业务域名分开,所以也会受到影响
  • 一些用于防止恶意请求的系统,对判断为恶意请求的访问会弹出验证码让用户进行安全验证,通过安全验证后会在请求所在域设置一个 Cookie,请求中带上这个 Cookie 之后,短时间内不再弹安全验证码,在 Chrome 80 以上如果因为 Samesite 的原因请求没办法带上这个 Cookie,则会出现一直弹出验证码进行安全验证
  • 某些请求了跨域的接口,因为没有 Cookie,接口不会返回数据
解决方法

解决方案就是设置 SameSitenone,以 Adobe 网站为例 www.adobe.com/sea/,查看请求可以看到 SameSite 是为 none 的,如下图

这里也有两点我们需要注意的地方

  • HTTP 接口不支持 SameSite=none

如果想加 SameSite=none 属性,那么该 Cookie 就必须同时加上 Secure 属性,表示只有在 HTTPS 协议下该 Cookie 才会被发送

  • 需要 UA 检测,部分浏览器不能加 SameSite=none

iOS 12Safari 以及老版本的一些 Chrome 会把 SameSite=none 识别成 SameSite=Strict,所以服务端必须在下发 Set-Cookie 响应头时进行 User-Agent 检测,对这些浏览器不下发 SameSite=none 属性

实例

我们下面来看一个实际的使用示例,即 express 中的 Cookie 是如何使用的,这里需要注意的是 express4.x 版本之后,Session 的管理和 Cookies 等许多模块都不再直接包含在 express 中,而是需要单独添加相应模块,在 express4.x 版本中操作 Cookie 可以使用 cookie-parser 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var express = require('express')
var cookieParser = require('cookie-parser')

var app = express()
app.listen(3000)

// 使用 cookieParser 中间件,cookieParser(secret, options)
// 其中 secret 用来加密 Cookie 字符串(下面会提到 signedCookies)
// options 传入上面介绍的 Cookie 可选参数
app.use(cookieParser())

app.get('/', function (req, res) {
// 如果请求中的 Cookie 存在 isVisit, 则输出 Cookie
// 否则,设置 Cookie 字段 isVisit, 并设置过期时间为1分钟
if (req.cookies.isVisit) {
console.log(req.cookies)
res.send('再次欢迎访问')
} else {
res.cookie('isVisit', 1, { maxAge: 60 * 1000 })
res.send('欢迎第一次访问')
}
})

什么是 Session

Cookie 虽然很方便,但是使用 Cookie 有一个很大的弊端,那就是 Cookie 中的所有数据在客户端就可以被修改,数据非常容易被伪造,那么一些重要的数据就不能存放在 Cookie 中了,而且如果 Cookie 中数据字段太多会影响传输效率,为了解决这些问题,就产生了 SessionSession 是另一种记录服务器和客户端会话状态的机制,Session 是基于 Cookie 实现的,Session 中的数据是保留在服务器端的,流程可以如下图所示

Session 的运作通过一个 SessionId 来进行,SessionId 通常是存放在客户端的 Cookie 中(比如在 express 中,默认是 connect.sid 这个字段),当请求到来时,服务端检查 Cookie 中保存的 SessionId 并通过这个 SessionId 与服务器端的 SessionData 关联起来,进行数据的保存和修改

这意思就是说,当你浏览一个网页时,服务端随机产生一个字符串,然后存在你 Cookie 中的 connect.sid 字段中,当你下次访问时,Cookie 会带有这个字符串,然后浏览器就知道你是上次访问过的某某某,然后从服务器的存储中取出上次记录在你身上的数据,由于字符串是随机产生的,而且位数足够多,所以也不担心有人能够伪造

Session 可以存放在内存、Cookie 本身、Redis 等缓存,又或者可以放置于数据库中,如果是线上环境,缓存的方案比较常见,如果是存在数据库的话,查询效率相比前三者都太低,不太推荐,而 Cookie-Session 有安全性问题,下面我们就来看看几种存储 Session 方式之间的区别以及一些实际使用的示例

在内存中存储 Session

express 当中操作 Session 要用到 express-session 这个模块,主要的方法就是 session(options),其中 options 中包含可选参数,主要有下面这些

  1. name,保存 Session 的字段名称,默认为 connect.sid
  2. storeSession 的存储方式,默认存放在内存中,也可以使用 redismongodb 等,express 生态中都有相应模块的支持
  3. secret,通过设置的 secret 字符串,来计算 Hash 值并放在 Cookie 中,使产生的 signedCookie 防篡改
  4. Cookie,设置存放 SessionIdCookie 的相关选项,默认为(default: { path: '/', HTTPOnly: true, secure: false, maxAge: null }
  5. genid,产生一个新的 SessionId 时,所使用的函数, 默认使用 uid2 这个 npm
  6. rolling,每个请求都重新设置一个 Cookie,默认为 false
  7. resave,即使 Session 没有被修改,也保存 Session 值,默认为 true

express-session 默认使用内存来存 Session,对于开发调试来说很方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var express = require('express')
var session = require('express-session')

var app = express()
app.listen(5000)

// 按照上面的解释,设置 Session 的可选参数
app.use(session({
secret: 'recommand 128 bytes random string', // 建议使用 128 个字符的随机字符串
cookie: { maxAge: 60 * 1000 }
}))

app.get('/', function (req, res) {
// 检查 Session 中的 isVisit 字段
// 如果存在则增加一次,否则为 Session 设置 isVisit 字段,并初始化为 1
if (req.session.isVisit) {
req.session.isVisit++
res.send('<p>第 ' + req.session.isVisit + '次来此页面</p>')
} else {
req.session.isVisit = 1
res.send('欢迎第一次来这里')
console.log(req.session)
}
})

在 Redis 中存储 Session

Session 存放在内存中不方便进程间共享,因此可以使用 Redis 等缓存来存储 Session,假设你的机器是四核的,你使用了四个进程在跑同一个服务,当用户访问进程一时,他被设置了一些数据当做 Session 存在内存中,而下一次访问时,他被负载均衡到了进程二,则此时进程二的内存中没有他的信息,认为他是个新用户,这就会导致用户在服务中的状态不一致

所以在这种情况下我们可以考虑使用 Redis 作为缓存,可以使用 connect-redis 模块来得到 Redis 连接实例,然后在 Session 中设置存储方式为该实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var express = require('express')
var session = require('express-session')
var redisStore = require('connect-redis')(session)

var app = express()
app.listen(5000)

app.use(session({
// 假如你不想使用 Redis 而想要使用 memcached 的话,代码改动也不会超过 5 行
// 这些 store 都遵循着统一的接口,凡是实现了那些接口的库,都可以作为 Session 的 store 使用
// 比如都需要实现 .get(keyString) 和 .set(keyString, value) 方法,编写自己的 store 也很简单
store: new redisStore(),
secret: 'somesecrettoken'
}))

app.get('/', function (req, res) {
if (req.session.isVisit) {
req.session.isVisit++
res.send('<p>第 ' + req.session.isVisit + '次来到此页面</p>')
} else {
req.session.isVisit = 1
res.send('欢迎第一次来这里')
}
})

我们可以运行 redis-cli 查看结果,如图可以看到 Redis 中缓存结果

各种存储的利弊

上面我们说到,Sessionstore 有四个常用选项

  1. 内存,其实在开发环境存内存就可以,一般的小程序为了省事,如果不涉及状态共享的问题,用内存 Session 也没问题,但内存 Session 除了省事之外,没有别的好处
  2. CookieCookie-Session 我们下面会提到,先说说利弊,用 Cookie-Session 的话,是不用担心状态共享问题的,因为 Sessiondata 不是由服务器来保存,而是保存在用户浏览器端,每次用户访问时,都会主动带上他自己的信息,它的弊端是增大了数据量传输,利端是方便
  3. 缓存,缓存方式是最常为常用用的方式,不仅速度快,而且又能共享状态,相比 Cookie-Session 来说,当 SessionData 比较大的时候,可以节省网络传输,也是推荐使用的方式
  4. 数据库,关于数据库 Session 除非你很熟悉这一块,知道自己要什么,否则还是使用缓存吧

signedCookie

Cookie 虽然很方便,但是使用 Cookie 有一个很大的弊端,即 Cookie 中的所有数据在客户端就可以被修改,数据非常容易被伪造,比如我们现在有一个网站,使用 Cookie 来记录登录的用户凭证,相应的 Cookie 长这样, dotcom_user=zhangsan,它说明现在的用户是 zhangsan 这个用户,如果我在浏览器中装个插件,把它改成 dotcom_user=lisi,服务器一读取,就会误认为我是 lisi,然后我就可以进行 lisi 才能进行的操作了

现在我们有一些数据,不想存在 Session 中,想存在 Cookie 中,怎么保证不被篡改呢?答案很简单,签个名,假设我们的服务器有个秘密字符串,是 this_is_my_secret,然后我们就可以为用户 Cookiedotcom_user 字段设置了个值 zhangsanCookie 本应是

1
{ 'dotcom_user': 'zhangsan' }

而如果我们签个名,比如把 dotcom_user 的值跟我们的秘密字符串做个 SHA1

1
sha1('this_is_my_secret' + 'zhangsan') === '59ea8588929a887ff79757a5f3fe8ae57f5df104'

然后把 Cookie 变成这样

1
2
3
4
{
'dotcom_user': 'zhangsan',
'dotcom_user.sig': '59ea8588929a887ff79757a5f3fe8ae57f5df104',
}

这样一来,用户就没法伪造信息了,一旦它更改了 Cookie 中的信息,则服务器会发现 Hash 校验的不一致,毕竟他不懂我们的秘密字符串是什么,而暴力破解哈希值的成本太高

Cookie-Session 的实现跟 signedCookies 差不多,不过 Cookie-Session 建议不要轻易使用,有受到回放攻击的危险,回放攻击指的是,比如一个用户,它现在有 100 积分,积分存在 Session 中,Session 保存在 Cookie 中,他先复制下现在的这段 Cookie,然后去发个帖子,扣掉了 20 积分,于是他就只有 80 积分了,而他现在可以将之前复制下的那段 Cookie 再粘贴回去浏览器中,于是服务器在一些场景下会认为他又有了 100 积分

如果避免这种攻击呢?这就需要引入一个第三方的手段来验证 Cookie-Session,而验证所需的信息,一定不能存在 Cookie 中,这么一来,避免了这种攻击后,使用 Cookie-Session 的好处就荡然无存了,如果为了避免攻击而引入了缓存使用的话,那不如把 Cookie-Session 也一起放进缓存中

什么是 Token

Acesss Token 简单来说就是访问资源接口(API)时所需要的资源凭证,Token 是由 uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,Token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)等几部分组成,它的特点是服务端无状态化、可扩展性好,支持移动端设备,相对而言比较安全,又支持跨程序调用,Token 的身份验证流程可以如下图所示

它的流程大致是下面这样的

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token 并把这个 Token 发送给客户端
  4. 客户端收到 Token 以后,会把它存储起来,比如放在 Cookie 里或者 localStorage
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

下面是一些使用 Token 的注意事项

  • 每一次请求都需要携带 Token,需要把 Token 放到 HTTPHeader
  • 基于 Token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 Token 数据,用解析 Token 的计算时间换取 Session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
  • Token 完全由应用管理,所以它可以避开同源策略

Token 和 Session 的区别

  • Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息,而 Token 是令牌,访问资源接口(API)时所需要的资源凭证,Token 使服务端无状态化,不会存储会话信息
  • SessionToken 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了,如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态
  • 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionId 的不可预测性,暂且认为是安全的,而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是认证和授权,认证是针对用户,授权是针对 App,其目的是让某 App 有权利访问某用户的信息,这里的 Token 是唯一的,不可以转移到其它 App上,也不可以转到其它用户上,Session 只提供一种简单的认证,即只要有此 SessionId ,即认为有此 User 的全部权利,是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App,所以简单来说,如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token ,如果永远只是自己的网站,自己的 App,用什么就无所谓了

什么是 JWT

JSON Web Token(简称 JWT)是一种认证授权机制,也是目前最流行的跨域认证解决方案,JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519),JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,比如用在用户登录上,可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名,因为数字签名的存在,这些传递的信息是可信的

我们这里只是简单介绍,更为关于 JWT 的内容可以参考 JSON Web Token 入门教程JWT 的原理是下面这样的

  1. 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
  2. 客户端将 Token 保存到本地(通常使用 localStorage,也可以使用 Cookie
  3. 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用 Bearer 模式添加 JWT,其内容看起来是下面这样的
1
Authorization: Bearer <token>

一些注意事项

  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
  • 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
  • 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS
  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

Token 和 JWT 的区别

相同点如下

  • 都是访问资源的令牌
  • 都可以记录用户的信息
  • 都是使服务端无状态化
  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源

区别如下

  • Token,服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效
  • JWT,将 TokenPayload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据

常见的加密算法

我们在之前的存储 Session 部分提到了一种加密方式 SHA1 其实是一种哈希算法,下面我们就来看看一些常见的加密算法,见下图

哈希算法(Hash Algorithm)又称散列算法、散列函数、哈希函数,是一种从任何一种数据中创建小的数字指纹的方法,哈希算法将数据重新打乱混合,重新创建一个哈希值,哈希算法主要用来保障数据真实性(即完整性),即发信人将原始消息和哈希值一起发送,收信人通过相同的哈希函数来校验原始数据是否真实,哈希算法通常有以下几个特点

  • 正像快速,原始数据可以快速计算出哈希值
  • 逆向困难,通过哈希值基本不可能推导出原始数据
  • 输入敏感,原始数据只要有一点变动,得到的哈希值差别很大
  • 冲突避免,很难找到不同的原始数据得到相同的哈希值

但是也有一些需要注意的地方

  1. 上面提到的一些算法不能保证数据被恶意篡改,原始数据和哈希值都可能被恶意篡改,要保证不被篡改可以使用 RSA 公钥私钥方案,再配合哈希值
  2. 哈希算法主要用来防止计算机传输过程中的错误

常见问题

下面我们来看一些比较常见的问题

  • 因为存储在客户端,容易被客户端篡改,使用前需要验证合法性
  • 不要存储敏感数据,比如用户密码,账户余额
  • 使用 httpOnly 在一定程度上提高安全性
  • 尽量减少 Cookie 的体积,能存储的数据量不能超过 4kb
  • 设置正确的 domainpath,减少数据传输
  • Cookie 无法跨域
  • 一个浏览器针对一个网站最多存 20Cookie,浏览器一般只允许存放 300Cookie

使用 Session 时需要考虑的问题

  • Session 存储在服务器里面,当用户同时在线量比较多时,这些 Session 会占据较多的内存,需要在服务端定期的去清理过期的 Session
  • 当网站采用集群部署的时候,会遇到多台 Web 服务器之间如何做 Session 共享的问题,因为 Session 是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建 Session 的服务器,那么该服务器就无法拿到之前已经放入到 Session 中的登录凭证之类的信息了
  • 当多个应用要共享 Session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 Cookie 跨域的处理
  • SessionId 是存储在 Cookie 中的,假如浏览器禁止 Cookie 或不支持 Cookie 怎么办? 一般会把 SessionId 跟在 URL 参数后面即重写 URL,所以 Session 不一定非得需要靠 Cookie 实现
  • 移动端对 Cookie 的支持不是很好,而 Session 需要基于 Cookie 实现,所以移动端常用的是 Token

使用 Token 时需要考虑的问题

  • 如果你认为用数据库来存储 Token 会导致查询时间太长,可以选择放在内存当中,比如 Redis 很适合你对 Token 查询的需求
  • Token 完全由应用管理,所以它可以避开同源策略
  • Token 可以避免 CSRF 攻击(因为不需要 Cookie 了)
  • 移动端对 Cookie 的支持不是很好,而 Session 需要基于 Cookie 实现,所以移动端常用的是 Token

使用 JWT 时需要考虑的问题

  • 因为 JWT 并不依赖 Cookie,所以可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS
  • JWT 默认是不加密,但也是可以加密的,生成原始 Token 以后,可以用密钥再加密一次
  • JWT 不仅可以用于认证,也可以用于交换信息,有效使用 JWT,可以降低服务器查询数据库的次数
  • JWT 最大的优势是服务器不再需要存储 Session,使得服务器认证鉴权业务可以方便扩展,但这也是 JWT 最大的缺点,由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限,也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑
  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限,为了减少盗用,JWT的有效期应该设置得比较短,对于一些比较重要的权限,使用时应该再次对用户进行认证
  • JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT,真正实现无状态
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输

使用加密算法时需要考虑的问题

  • 永远使用哈希算法来处理密码,不要使用 Base64 或其他编码方式来存储密码,这和以明文存储密码是一样的
  • 绝不要使用弱哈希或已被破解的哈希算法,像 MD5SHA1,只使用强密码哈希算法
  • 绝不要以明文形式显示或发送密码,即使是对密码的所有者也应该这样,如果你需要忘记密码的功能,可以随机生成一个新的一次性的(这点很重要)密码,然后把这个密码发送给用户

只要关闭浏览器 Session 就消失了吗

不会,因为对 Session 来说,除非程序通知服务器删除一个 Session,否则服务器会一直保留,程序一般都是在用户做退出操作的时候发个指令去删除 Session,然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 Session 机制都使用会话 Cookie 来保存 SessionId

而关闭浏览器后这个 SessionId 就消失了,再次连接服务器时也就无法找到原来的 Session,如果服务器设置的 Cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 SessionId 发送给服务器,则再次打开浏览器仍然能够打开原来的 Session,恰恰是由于关闭浏览器不会导致 Session 被删除,迫使服务器为 Session 设置了一个失效时间,当距离客户端上一次使用 Session 的时间超过这个失效时间时,服务器就认为客户端已经停止了活动,才会把 Session 删除以节省存储空间

参考

# HTTP

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×