深度理解token

一、token是什么?

token是一种用户标识,表示用户身份,相似于咱们的身份证件。
Token 是在服务端产生的。若是前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端能够在每次请求的时候带上 Token 证实本身的合法地位。若是这个 Token 在服务端持久化(好比存入数据库),那它就是一个永久的身份令牌。前端

二、为何要使用token?

先来了解一下token认证与session认证的区别:
a) Session认证Session认证的方案为:第一次用户认证请求经过时,服务器端存储用户的登陆信息,而后在响应时传递给浏览器,浏览器保存为cookie,下次请求时把cookie发送给服务器,服务器根据cookie信息来识别是哪一个用户。Session认证适用于单个服务器的场合,若是用户增多,须要部署多个服务器时,Session认证就会暴露问题。由于每一个用户在认证后,服务器都会记录Session,通常保存在内存中,随着认证用户增多,服务器开销也会增大;并且认证用户的后续请求都须要到这台保存了本身Session的服务器上验证Cookie。在多集群分布式的场合,这就形成了性能瓶颈,对负载均衡、应用的扩展都会形成影响。(不能多服务共享)进程外Session经过将Session保存在数据库或硬盘中能够解决服务器内存开销的问题,但仍然不便于扩展。web

b) Token认证Token认证也是无状态的,不须要在服务端保存认证用户的信息,它的认证流程为:用户使用用户名密码请求服务器;服务器验证用户,若是验证经过就发给用户一个token;客户端保存token,并在以后的每次请求都附上token;服务端验证token、返回数据。Token认证机制不须要保存认证用户的信息、也不须要考录用户在那台服务器登陆,能够很好地适应应用的扩展(能够多服务共享)。token的体积很小,请求服务端时,能够被附在URL、Header或是Post参数中。并且token中包含了用户相关的必要、非私密的相信,免去了服务端再次查询数据库的开销
另外,token认证能解决如下问题:算法

  • Token 彻底由应用管理,因此它能够避开同源策略
  • Token 能够避免 CSRF 攻击
  • Token 能够是无状态的,能够在多个服务间共享

如今详细说明如下roken为何可以避免CSRF攻击呢?CSRF攻击又是什么意思?数据库

2.一、CSRF攻击

CSRF(Cross-site request forgery)也被称为 one-click attack或者 session riding,中文全称是叫跨站请求伪造。通常来讲,攻击者经过伪造用户的浏览器的请求,向访问一个用户本身曾经认证访问过的网站发送出去,使目标网站接收并误觉得是用户的真实操做而去执行命令。经常使用于盗取帐号、转帐、发送虚假消息等。攻击者利用网站对请求的验证漏洞而实现这样的攻击行为,网站可以确认请求来源于用户的浏览器,却不能验证请求是否源于用户的真实意愿下的操做行为。json

2.二、token如何避免CSRF攻击

CSRF 攻击之因此可以成功,是由于黑客能够彻底伪造用户的请求,该请求中全部的用户验证信息都是存在于 cookie 中,所以黑客能够在不知道这些验证信息的状况下直接利用用户本身的 cookie 来经过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,而且该信息不存在于 cookie 之中。能够在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端创建一个拦截器来验证这个 token,若是请求中没有 token 或者 token 内容不正确,则认为多是 CSRF 攻击而拒绝该请求。后端

三、使用JWT生成和验证token

如下代码仅仅用于示意,并非完整版。浏览器

3.一、采用HS256算法生成和验证

后台生成token:缓存

const jwt = require('jsonwebtoken')
const fs = require('fs')

// Token 数据
const payload = {
  name: 'wanghao',
  admin: true
}

/**
 * HS256
 * 用 HS256 算法生成与验证 JWT
 */

// 密钥
const secret = 'ILOVENINGHAO'

// 签发 Token
const token = jwt.sign(payload, secret, { expiresIn: '1day' })

// 输出签发的 Token
console.log('HS256 算法:', token)

拦截器验证:安全

// 验证 Token
jwt.verify(token, secret, (error, decoded) => {
  if (error) {
    console.log(error.message)
    return
  }
  console.log(decoded)//输出解码后的数据
})

3.二、采用RS256算法生成和验证

后台生成:服务器

/**
 * RS256
 * 用 RS256 算法生成与验证 JWT
 * ssh-keygen -t rsa -b 2048 -f private.key//生成私钥
 * openssl rsa -in private.key -pubout -outform PEM -out public.key//生成公钥
 */

// 获取签发 JWT 时须要用的密钥
const privateKey = fs.readFileSync('./config/private.key')
// 签发 Token
const tokenRS256 = jwt.sign(payload, privateKey, { algorithm: 'RS256' })

// 输出签发的 Token
console.log('RS256 算法:', tokenRS256)

// 获取验证 JWT 时须要用的公钥
const publicKey = fs.readFileSync('./config/public.key')

拦截器验证:

// 验证 Token
jwt.verify(tokenRS256, publicKey, (error, decoded) => {
  if (error) {
    console.log(error.message)
    return
  }
  console.log(decoded)
})

四、token进阶

须要设置有效期吗?

对于这个问题,咱们不妨先看两个例子。一个例子是登陆密码,通常要求按期改变密码,以防止泄漏,因此密码是有有效期的;另外一个例子是安全证书。SSL 安全证书都有有效期,目的是为了解决吊销的问题,对于这个问题的详细状况,来看看知乎的回答(http://dwz.cn/7joMhq)。因此不管是从安全的角度考虑,仍是从吊销的角度考虑,Token 都须要设有效期。

那么有效期多长合适呢?

只能说,根据系统的安全须要,尽量的短,但也不能短得离谱——想像一下手机的自动熄屏时间,若是设置为 10 秒钟无操做自动熄屏,再次点亮须要输入密码,会不会疯?若是你以为不会,那就亲自试一试,设置成能够设置的最短期,坚持一周就好(不排除有人适应这个时间,毕竟手机厂商也是有用户体验研究的)。

而后新问题产生了,若是用户在正常操做的过程当中,Token 过时失效了,要求用户从新登陆……用户体验岂不是很糟糕?

为了解决在操做过程不能让用户感到 Token 失效这个问题,有一种方案是在服务器端保存 Token 状态,用户每次操做都会自动刷新(推迟) Token 的过时时间——Session 就是采用这种策略来保持用户登陆状态的。然而仍然存在这样一个问题,在先后端分离、单页 App 这些状况下,每秒种可能发起不少次请求,每次都去刷新过时时间会产生很是大的代价。若是 Token 的过时时间被持久化到数据库或文件,代价就更大了。因此一般为了提高效率,减小消耗,会把 Token 的过时时保存在缓存或者内存中。

还有另外一种方案,使用 Refresh Token,它能够避免频繁的读写操做。这种方案中,服务端不须要刷新 Token 的过时时间,一旦 Token 过时,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。这种方案中,服务端只须要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减小了更新有效期的操做,也就避免了频繁读写。固然 Refresh Token 也是有有效期的,可是这个有效期就能够长一点了,好比,以天为单位的时间。

时序图表示

使用 Token 和 Refresh Token 的时序图以下:

1)登陆

图片描述

2)业务请求
图片描述

3)Token 过时,刷新 Token
图片描述

上面的时序图中并未提到 Refresh Token 过时怎么办。不过很显然,Refresh Token 既然已通过期,就该要求用户从新登陆了。

固然还能够把这个机制设计得更复杂一些,好比,Refresh Token 每次使用的时候,都更新它的过时时间,直到与它的建立时间相比,已经超过了很是长的一段时间(好比三个月),这等因而在至关长一段时间内容许 Refresh Token 自动续期。

到目前为止,Token 都是有状态的,即在服务端须要保存并记录相关属性。那说好的无状态呢,怎么实现?

无状态 Token

若是咱们把全部状态信息都附加在 Token 上,服务器就能够不保存。可是服务端仍然须要认证 Token 有效。不过只要服务端能确认是本身签发的 Token,并且其信息未被改动过,那就能够认为 Token 有效——“签名”能够做此保证。平时常说的签名都存在一方签发,另外一方验证的状况,因此要使用非对称加密算法。可是在这里,签发和验证都是同一方,因此对称加密算法就能达到要求,而对称算法比非对称算法要快得多(可达数十倍差距)。

更进一步思考,对称加密算法除了加密,还带有还原加密内容的功能,而这一功能在对 Token 签名时并没有必要——既然不须要解密,摘要(散列)算法就会更快。能够指定密码的散列算法,天然是 HMAC。

上面说了这么多,还须要本身去实现吗?不用!JWT 已经定义了详细的规范,并且有各类语言的若干实现。

不过在使用无状态 Token 的时候在服务端会有一些变化,服务端虽然不保存有效的 Token 了,却须要保存未到期却已注销的 Token。若是一个 Token 未到期就被用户主动注销,那么服务器须要保存这个被注销的 Token,以便下次收到使用这个仍在有效期内的 Token 时判其无效。有没有感到一点沮丧?

在前端可控的状况下(好比前端和服务端在同一个项目组内),能够协商:前端一但注销成功,就丢掉本地保存(好比保存在内存、LocalStorage 等)的 Token 和 Refresh Token。基于这样的约定,服务器就能够假设收到的 Token 必定是没注销的(由于注销以后前端就不会再使用了)。

若是前端不可控的状况,仍然能够进行上面的假设,可是这种状况下,须要尽可能缩短 Token 的有效期,并且必须在用户主动注销的状况下让 Refresh Token 无效。这个操做存在必定的安全漏洞,由于用户会认为已经注销了,实际上在较短的一段时间内并无注销。若是应用设计中,这点漏洞并不会形成什么损失,那采用这种策略就是可行的。

在使用无状态 Token 的时候,有两点须要注意:

Refresh Token 有效时间较长,因此它应该在服务器端有状态,以加强安全性,确保用户注销时可控

应该考虑使用二次认证来加强敏感操做的安全性

到此,关于 Token 的话题彷佛差很少了——然而并无,上面说的只是认证服务和业务服务集成在一块儿的状况,若是是分离的状况呢?

分离认证服务

当 Token 无状态以后,单点登陆就变得容易了。前端拿到一个有效的 Token,它就能够在任何同一体系的服务上认证经过——只要它们使用一样的密钥和算法来认证 Token 的有效性。就样这样:
图片描述

固然,若是 Token 过时了,前端仍然须要去认证服务更新 Token:

图片描述

可见,虽然认证和业务分离了,实际即并没产生多大的差别。固然,这是创建在认证服务器信任业务服务器的前提下,由于认证服务器产生 Token 的密钥和业务服务器认证 Token 的密钥和算法相同。换句话说,业务服务器一样能够建立有效的 Token。

若是业务服务器不能被信任,该怎么办?

不受信的业务服务器

遇到不受信的业务服务器时,很容易想到的办法是使用不一样的密钥。认证服务器使用密钥1签发,业务服务器使用密钥2验证——这是典型非对称加密签名的应用场景。认证服务器本身使用私钥对 Token 签名,公开公钥。信任这个认证服务器的业务服务器保存公钥,用于验证签名。幸亏,JWT 不只可使用 HMAC 签名,也可使用 RSA(一种非对称加密算法)签名。

不过,当业务服务器已经不受信任的时候,多个业务服务器之间使用相同的 Token 对用户来讲是不安全的。由于任何一个服务器拿到 Token 均可以仿冒用户去另外一个服务器处理业务……悲剧随时可能发生。

为了防止这种状况发生,就须要在认证服务器产生 Token 的时候,把使用该 Token 的业务服务器的信息记录在 Token 中,这样当另外一个业务服务器拿到这个 Token 的时候,发现它并非本身应该验证的 Token,就能够直接拒绝。

如今,认证服务器不信任业务服务器,业务服务器相互也不信任,但前端是信任这些服务器的——若是前端不信任,就不会拿 Token 去请求验证。那么为何会信任?多是由于这些是同一家公司或者同一个项目中提供的若干服务构成的服务体系。

可是,前端信任不表明用户信任。若是 Token 不没有携带用户隐私(好比姓名),那么用户不会关心信任问题。但若是 Token 含有用户隐私的时候,用户得关心信任问题了。这时候认证服务就不得再也不啰嗦一些,当用户请求 Token 的时候,问上一句,你真的要受权给某某某业务服务吗?而这个“某某某”,用户怎么知道它是否是真的“某某某”呢?用户固然不知道,甚至认证服务也不知道,由于公钥已经公开了,任何一个业务均可以声明本身是“某某某”。

为了获得用户的信任,认证服务就不得不帮助用户来甄别业务服务。因此,认证服器决定不公开公钥,而是要求业务服务先申请注册并经过审核。只有经过审核的业务服务器才能获得认证服务为它建立的,仅供它使用的公钥。若是该业务服务泄漏公钥带来风险,由该业务服务自行承担。如今认证服务能够清楚的告诉用户,“某某某”服务是什么了。若是用户仍是不够信任,认证服务甚至能够问,某某某业务服务须要请求 A、B、C 三项我的数据,其中 A 是必须的,否则它不工做,是否容许受权?若是你受权,我就把你受权的几项数据加密放在 Token 中……

相关文章
相关标签/搜索