认证授权——JWT

一、Token

token 是一串字符串,通常因为作为鉴权凭据,最常用的使用场景是 API 鉴权。

1. API 鉴权

那么 API 鉴权一般有几种方式呢?我大概整理了如下:

cookie + session

和平常 web 登陆一样的鉴权方式,很常见,不再赘述。

HTTP Basic

将账号和密码拼接然后 base64 编码加到 header 头中。很显然,因为账号和密码几乎是『明文』传输的,而且每次请求都传,安全性可想而知。

HTTP Digest

将账号和密码加上其他一些信息拼接然后取摘要加到 header 头中。这个安全性比上面要好一点,因为如果是取摘要的话,即使信息段被截取,也无法轻易破解出来(当然也是有破解的可能)。

不过其实最大的问题还是:每次请求都要对账号、密码取一次摘要,也就是说每次请求都要有账号和密码,也就是说账号和密码要么缓存一下,要么就每次请求要去用户输一次密码,这样显然不合适。同样,上面的 Basic 也存在这样的问题。

Token

token 通过一次登录验证,得到一个鉴权字符串,然后以后带着这个鉴权字符串进行后续操作,这样就可以解决每次请求都要带账号密码的问题,而且也不需要反复使用账号和密码。

2. Token 的优势

token 相对于 Cookie + Session 的优点

  1. 无状态:token 自身包含了身份验证所需要的所有信息,使得我们的服务器不需要存储 Session 信息,这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。但是,也正是由于 token 的无状态,也导致了它最大的缺点:当后端在token 有效期内废弃一个 token 或者更改它的权限的话,不会立即生效,一般需要等到有效期过后才可以。另外,当用户 Logout 的话,token 也还有效。除非,我们在后端增加额外的处理逻辑。

  2. 适合移动端应用:使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。但是,使用 token 进行身份认证就不会存在这种问题,因为只要 token 可以被客户端存储就能够使用,而且 token 还可以跨语言使用。

  3. 单点登录友好:使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 token 进行认证的话, token 被保存在客户端,不会存在这些问题。

  4. 防CSRF 攻击:这个原理不多做介绍,构成这个攻击的原因,就在于 Cookie + Session 的鉴权方式中,鉴权数据(cookie 中的 session_id)是由浏览器自动携带发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。而 token 是通过客户端本身逻辑作为动态参数加到请求中的,token 也不会轻易泄露出去,因此 token 在 CSRF 防御方面存在天然优势。

3. Token 的种类

一般来说 token 主要三种:

  • 自定义的 token:开发者根据业务逻辑自定义的 token
  • JWT:JSON Web Token,定义在 RFC 7519 中的一种 token 规范
  • Oauth2.0:定义在 RFC 6750 中的一种授权规范,但这其实并不是一种 token,只是其中也有用到 token

二、JWT 的组成和优势

JWT 全称 JSON Web Tokens ,是一种规范化的 token。可以理解为对 token 这一技术提出一套规范,是在 RFC 7519 中提出的。

1.组成

一个 JWT token 是一个字符串,它由三部分组成,头部、载荷与签名,中间用 . 分隔,例如:xxxxx.yyyyy.zzzzz

头部(header)

头部通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法(如 HMAC SHA256 或 RSA.)。如下

{
  "alg": "HS256",
  "typ": "JWT"
}

然后用 Base64Url 编码得到头部,即 xxxxx。

载荷(Payload)

载荷中放置了 token 的一些基本信息,以帮助接受它的服务器来理解这个 token。同时还可以包含一些自定义的信息,用户信息交换。

载荷的属性也分三类:

  • 预定义(Registered)
  • 公有(public)
  • 私有(private)

预定义的载荷

{
  "sub": "1",
  "iss": "http://localhost:8000/auth/login",
  "iat": 1451888119,
  "exp": 1454516119,
  "nbf": 1451888119,
  "jti": "37c107e4609ddbcc9c096ea5ee76c667",
  "aud": "dev"
}

这里面的前 7 个字段都是由官方所定义的,也就是预定义(Registered claims)的,并不都是必需的。

iss (issuer):签发人
sub (subject):主题
aud (audience):受众
exp (expiration time):过期时间
nbf (Not Before):生效时间,在此之前是无效的
iat (Issued At):签发时间
jti (JWT ID):编号

公有的载荷

在使用 JWT 时可以额外定义的载荷。为了避免冲突,应该使用 IANA JSON Web Token Registry 中定义好的,或者给额外载荷加上类似命名空间的唯一标识。

私有载荷

在信息交互的双方之间约定好的,既不是预定义载荷也不是公有载荷的一类载荷。这一类载荷可能会发生冲突,所以应该谨慎使用。

将上面的 json 进行 Base64Url 编码得到载荷,,即 yyyyy。

签名(Signature)

签名时需要用到前面编码过的两个字符串,如果以 HMACSHA256 加密,就如下:

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

加密后再进行 base64url 编码最后得到的字符串就是 token 的第三部分 zzzzz。

组合便可以得到 token:xxxxx.yyyyy.zzzzz。

签名的作用:保证 JWT 没有被篡改过,原理如下:

HMAC 算法是不可逆算法,类似 MD5 和 hash ,但多一个密钥,密钥(即上面的 secret)由服务端
有,客户端把 token 发给服务端后,服务端可以把其中的头部和载荷再加上事先共享的 secret 再进
行一次 HMAC 加密,得到的结果和 token 的第三段进行对比,如果一样则表明数据没有被篡改。

Hash-based Message Authentication Code

2.使用

JWT 的使用有两种方式:

  • 加到 url 中:?token=你的token
  • 加到 header 中,建议用这种,因为在 https 情况下更安全:Authorization:Bearer 你的token

JWT 在客户端的存储有三种方式:

  • LocalStorage
  • SessionStorage
  • Cookie [不能设置 HTTPonly]

但是最推荐的还是第三种,因为第一二种存在跨域读取限制,而 Cookie 使用不同的跨域策略

Cookie 的跨域策略

子可以读父,但是父不可以读子,兄弟之间不能互相访问。

a.xxx.com 和 b.xxx.com 可以读 xxx.com,但是 a.xxx.com 和 b.xxx.com 不能互相读取,xxx.com 也不能读 a.xxx.com 和 b.xxx.com 的。

3. 相对于一般 token 的优点

既然 JWT 也是一种 token,那么它相对于普通的 token 有何优点呢?

无状态

因为 JWT 的有效期完全与其载荷中编码的过期时间,服务端不维护任何状态,因此 JWT 『一般』是『无状态』的(为什么是一般,后面会仔细说)。无状态最大的优势在于三点:

  • 节省服务器的资源:因为服务端无需维护一个状态,因此能够节省服务端原先保存这些状态所花费的资源
  • 适合分布式:因为服务端无需维护状态,因此如果服务端是多台服务器组成的分布式集群,那么无需像『有状态』一样互相同步各自的状态。
  • 时间换空间:因为 token 的校验时通过签名校验来进行的,签名校验消耗的是 CPU 时间,而『有状态』是需要通过客户端提供的凭据对服务端现有的状态进行一次查询,消耗的是 I/O 和内存、磁盘空间。通常对于一个 Web 服务来说,其属于 I/O 密集型,因此通过时间换空间这一操作,可以提高整体的硬件使用率。

编码数据

因为 JWT 能够在载荷中编码了部分信息,所以如果把常用数据编码进去的话,能够大大减少数据库的查询次数,不过有两点需要额外注意的:

  • 载荷信息是明文编码的,所以不能编码敏感信息在里面,如果要编码可以先加密再编码进去
  • token 在每次请求时都会进行传输,所以载荷中不能编码过多的信息,否则会降低传输效率

三、JWT 的安全问题

既然主要使用场景是鉴权,那么安全问题就是不得不考虑的问题了。下面对 JWT 可能需要的安全问题都进行一次深入的探讨并寻求最佳的解决方案。

1.重放攻击

重放攻击是通过把原先的包进行一次重放来进行攻击的手段。需要先明确是的 cookie + session 也是存在重放攻击的问题的。

常用的防范重放攻击的措施主要有以下几种:

timestamp

在请求中夹带一个时间戳,设置较短的有效期,如果一个新来的请求的请求时间超过了请求中的有效期,则认为无效。但是这种策略也存在问题,即如果一个黑客『眼疾手快』在有效期以内将你的包进行了重放, 那就来攻击成功。

这种策略对应到 JWT 中就是给 token 设置一个较短的有效期。

nonce

在请求中夹带一个随机字符串,这个字符串传送到客户端后即存入客户端的黑名单中,如果一个新来的请求其中存在的随机字符串已经在黑名单中则认为无效。但是显然,这个策略存在巨大的问题:服务端需要维护一个黑名单库,这个库的大小会随着业务运行的时间而变得无比巨大,从而严重影响效率。

这种策略对应到 JWT 中就是给 token 设置一个黑名单,但是不设置有效期。

timestamp + nonce

在请求中夹带一个随机字符串和一个时间戳,如果一个新来的请求,其随机字符串已经在黑名单中则认为无效,或者一个请求的的请求时间超过了其有效期,则也认为其无效。这样黑名单的范围只需设置为时间戳策略的有效期范围即可。

这种策略对应到 JWT 中就是给 token 既设置一个黑名单,又设置一个有效期。

挑战 - 应答

这个其实和 timestamp + nonce 策略一样,只是随机字符串是有服务端生成给客户端的,客户端携带服务端所给的随机串来请求。这样有什么好处呢?服务端可以通过一个加密算法来生成这个串,使其和时间戳相关,同时客户端又无法伪造。这样就不需要维护黑名单了。同样也是时间换空间的策略。但是显然每次或几次请求就要进行一次与预请求以得到随机串,并不是特别方便,造成的额外消耗也有待考量。

序列号

通过在请求中嵌入一个序列号,每次请求依次加一,如果一个请求的序列号早已用过,则认为无效。但是这个要用逻辑额外一个全局序列号,并不是特别方便。

HTTPS

终极解决方案了,HTTPS 在握手过程中会自动维护一个隐式序列号,解决了上面要自己维护序列号的问题。

2. token 被盗

因为 token 中包含了登陆状态,因此一旦 token 被盗,那么就会被人盗用身份。那么 token 针对被盗的防范措施整理如下:

  • 使用 HTTPS 传输:从传输层的角度解决问题
  • HTTPOnly:从存储层的角度解决问题,防止 XSS 攻击窃取 cookie,但是这种方案其实存在问题,因为这样 js 就无法读取 token 并把它加到 header 头中了。所以不开启 HTTPOnly 的话必须要额外注意防范 XSS 攻击。
  • 在 token 中嵌入客户端指纹:通过客户端指纹,即使黑客盗取了你的 cookie,他也无法用你的 cookie 进行请求。
  • 设置较短的 token 有效期:这样如果 token 被盗,只要超过一定时限就无法使用。

四、JWT 的其他问题

除了安全问题,JWT 还有许多其他需要考虑的问题。

1. 注销问题

因为 JWT 是无状态的,所以它的有效期完全由其本身决定,也就是说服务端无法让一个 token 失效。显然这是一个比较大的问题,对此也有诸多解决方案:

1.1 客户端主动注销

客户端直接删除存储 token 的 cookie

这种方案最为简单,操作的结果是无论客户端还是服务端都没有这个 token,可问题是,这个 token 并没有真正不可使用,而是处于一个游离态。

黑名单策略

客户端携带要注销的 token 访问一个注销接口,服务端把 token 加入一个黑名单。

1.2 服务端主动注销 \ 用户修改密码

把 token 和 uuid 用 key-value 对存储在 redis

这种方案看上去没问题,但是实际上,相当于自己实现了一次 cookie + session,JWT 就失去了『无状态』这一特性,从也会失去『无状态』特性带来的一系列的优点。

让每个用户都有一个 secret

前面讲到签发 token 的时候用到了 secret ,这种策略的思想就是让每个用户都有一个 secret,注销一个用户的时候修改其 secret,即可使其前面签发的 token 无法通过校验而失效。

这种策略上听上去不需要维护一个状态,但是实际上存在更大的问题。试想一下,第一种方案是通过 uuid 在已登录用户的 token 表中找到要注销的 token 注销。cookie + session 是通过 session_id 在已登录的用户的 session 表中找到其对应的 session 并删除来注销。而此方案是通过 uuid 在所有用户(而非已登录用户)中找到对于的 secret 修改来注销。这样看来会发现效率更低,因为查找范围更大了。

预黑名单

把要注销的用户的 uuid 和当前时间(TIME) 组成 key-value 对加入预黑名单,下次请求来时,若其 uuid 和黑名单中的对应,并且签发时间在 TIME 之前,则将其注销。这样查找范围就是未过期但又要注销的用户。并且在实现逻辑上这个预黑名单可以和签名的黑名单做到一起。

2. 续签问题

token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?

我们先来看看在 Session 认证中一般的做法:假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期被延长30分钟。

类似于 Session 认证中的做法:

这种方案满足于大部分场景。假设服务端给的 token 有效期设置为30分钟,服务端每次进行校验时,如果发现 token 的有效期马上快过期了,服务端就重新生成 token 给客户端。客户端每次请求都检查新旧token,如果不一致,则更新本地的token。这种做法的问题是仅仅在快过期的时候请求才会更新 token ,对客户端不是很友好。

每次请求都返回新 token :

这种方案的的思路很简单,但是,很明显,开销会比较大。

token 有效期设置到半夜 :

这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。

用户登录返回两个 token :

第一个是 acessToken ,它的过期时间 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为1天。客户端登录后,将 accessToken和refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。

该方案的不足是:1. 需要客户端来配合;2.用户注销的时候需要同时保证两个 token 都无效;3. 重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的accessToken)。

五、JWT 适合用来做什么

1. 无状态的 RESTful API

这个显然很适合。

2. SSO 单点登录

单点登录必须要实现的:

  • 会话管理:通过黑名单和预黑名单解决
  • 续签:通过签名的解决方案解决

可见,对 JWT 部署一些额外逻辑(黑名单,续签管理)即可让 JWT 在大部分场景代替 cookie + session。

六、关于 token 十件必须知道的事

  1. Token 获取到后需要保存起来以便下次使用,可以选择存储在 localstorage /sessionstorage/cookie
  2. Token 是包含有效期的,你必须部署一些逻辑来进行有效期的控制
  3. localstorage /sessionstorage 的跨域限制较 cookie 更为严格,推荐使用 cookie
  4. 处理 XSS 比处理 CSRF 更容易(这一点我实在没看到他是什么个逻辑,大家可以去看看原文)
  5. token 在每次请求时都会被编码到请求中,所以请注意 token 的大小,不要编码过多数据
  6. 如果在 token 中编码敏感信息,请对 token 进行加密
  7. JSON Web Token 可以用于 Oauth2.0 的 Bearer Token 中,赋予 Oauth2.0 无状态的优势
  8. Token 不是银弹,请根据实际业务需要进行选择

实现参考:

基于Spring Security和 JWT的权限系统设计

基于Spring Security Oauth2的SSO单点登录+JWT权限控制实践

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值