从JWT到CSRF

1. JWT是什么

JSON Web Token(缩写 JWT),一个规范用于用户和服务器之间传递安全可靠的信息。原理为服务端生成一个带有用户标识的令牌返回给客户端,客户端请求的时候再带上这个令牌,服务端根据这个令牌来确认当前用户是谁。同时使用签名保证数据的安全性(不被篡改)。这样做就能省掉服务端的session记录,连用户状态都不需要再关心。


思考下可能存在的问题:
  • jwt泄露,导致用户信息泄露,以及操作权被使用
    1. jwt不能涉及用户关键数据,如果涉及需要将jwt进行加密。
    2. 缩短jwt有效期
    3. 使用https
    4. 重要服务进行二次验证
  • jwt存储在哪?客户端能否统一处理让所有请求带上这个令牌?
    1. 存储在cookie中,自动实现统一处理。但没法解决跨域问题
    2. 设在http-header中,如 Authorization: Bearer <token> 。所有的http请求包含表单、ajax、同步跳转都要加上这个header.
  • jwt伪造,如果签名密钥泄露出去了那么jwt就可以被伪造
    1. 签名密钥泄露并且被伪造token,这个在服务端是无法感知的。只能加强对secret的管理,并且对重要服务进行二次校验。
  • 后管怎么强制将某个jwt失效?无法做到类似踢出某个用户的动作
    1. 使用JWT的方式就是存在这种问题、无法解决。因为数据都不存储在服务端。一旦颁发token只有等待失效。

2. JWT结构

  • Header(头部)
  • Payload(消息体)
  • Signature(签名)

JWT由上面3个部分组成。

头信息指定了该JWT使用的签名算法:

header = '{"alg":"HS256","typ":"JWT"}'

HS256表示使用了 HMAC-SHA256 来生成签名。

消息体包含了令牌的意图:

payload = '{"sub":"admin","iat":1422779638}'//iat表示令牌生成的时间
  1. iss (issuer):签发人
  2. exp (expiration time):过期时间
  3. sub (subject):主题
  4. aud (audience):受众
  5. nbf (Not Before):生效时间
  6. iat (Issued At):签发时间
  7. jti (JWT ID):编号

签名是用base64url编码的头信息和消息体拼接(使用”.”分隔) 再通过私有的secret计算而来

key = 'secretkey'  
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)  
signature = HMAC-SHA256(key, unsignedToken)

最后得到整个token数据为

token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature) 

3. JWT使用场景

  1. 一次性验证
    比如用户注册后需要发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其他可能的账户…这种场景就和 jwt 的特性非常贴近,jwt 的 payload 中固定的参数:iss 签发者和 exp 过期时间正是为其做准备的。

  2. restful api的无状态认证
    使用 jwt 来做 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务端共享 secret;过期时间由服务端校验,客户端定时刷新;签名信息不可被修改…spring security oauth jwt 提供了一套完整的 jwt 认证体系,以笔者的经验来看:使用 oauth2 或 jwt 来做 restful api 的认证都没有大问题,oauth2 功能更多,支持的场景更丰富,后者实现简单(后者的实现还是需要覆盖认证的场景。如果不认证,jwt就没有用户标识)。

  3. 使用 jwt 做单点登录+会话管理(不推荐)

4. JWT特点

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

5. JWT和cookie-session对比

JWT更关注的是实体之间安全的消息传输。无状态快速响应(不涉及第三方存储).
cookie-session更侧重的是用户信息安全、基于http协议。

如果使用JWT实现单点登录,那么需要考虑以下几点问题。

secret如何设计

jwt 唯一存储在服务端的只有一个 secret,个人认为这个 secret 应该设计成和用户相关的,而不是一个所有用户公用的统一值。这样可以有效的避免一些注销和修改密码时遇到的窘境。

注销和修改密码

传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态保存在服务端。但 jwt 的方案就比较难办了,因为 jwt 是无状态的,服务端通过计算来校验有效性。没有存储起来,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不过处于一个游离状态。分析下痛点:注销变得复杂的原因在于 jwt 的无状态。我提供几个方案,视具体的业务来决定能不能接受。

  • 仅仅清空客户端的 cookie,这样用户访问时就不会携带 jwt,服务端就认为用户需要重新登录。这是一个典型的假注销,对于用户表现出退出的行为,实际上这个时候携带对应的 jwt 依旧可以访问系统。
  • 清空或修改服务端的用户对应的 secret,这样在用户注销后,jwt 本身不变,但是由于 secret 不存在或改变,则无法完成校验。这也是为什么将 secret 设计成和用户相关的原因。
  • 借助第三方存储自己管理 jwt 的状态,可以以 jwt 为 key,实现去 redis 一类的缓存中间件中去校验存在性。方案设计并不难,但是引入 redis 之后,就把无状态的 jwt 硬生生变成了有状态了,违背了 jwt 的初衷。实际上这个方案和 session 都差不多了。

修改密码则略微有些不同,假设号被到了,修改密码(是用户密码,不是 jwt 的 secret)之后,盗号者在原 jwt 有效期之内依旧可以继续访问系统,所以仅仅清空 cookie 自然是不够的,这时,需要强制性的修改 secret。在我的实践中就是这样做的。

续签问题

续签问题可以说是我抵制使用 jwt 来代替传统 session 的最大原因,因为 jwt 的设计中我就没有发现它将续签认为是自身的一个特性。传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟。而 jwt 本身的 payload 之中也有一个 exp 过期时间参数,来代表一个 jwt 的时效性,而 jwt 想延期这个 exp 就有点身不由己了,因为 payload 是参与签名的,一旦过期时间被修改,整个 jwt 串就变了,jwt 的特性天然不支持续签!

如果你一定要使用 jwt 做会话管理(payload 中存储会话信息),也不是没有解决方案,但个人认为都不是很令人满意

  • 每次请求刷新 jwt

jwt 修改 payload 中的 exp 后整个 jwt 串就会发生改变,那…就让它变好了,每次请求都返回一个新的 jwt 给客户端。太暴力了,不用我赘述这样做是多么的不优雅,以及带来的性能问题。

但,至少这是最简单的解决方案。

  • 只要快要过期的时候刷新 jwt

一个上述方案的改造点是,只在最后的几分钟返回给客户端一个新的 jwt。这样做,触发刷新 jwt 基本就要看运气了,如果用户恰巧在最后几分钟访问了服务器,触发了刷新,万事大吉;如果用户连续操作了 27 分钟,只有最后的 3 分钟没有操作,导致未刷新 jwt,无疑会令用户抓狂。

  • 完善 refreshToken

借鉴 oauth2 的设计,返回给客户端一个 refreshToken,允许客户端主动刷新 jwt。一般而言,jwt 的过期时间可以设置为数小时,而 refreshToken 的过期时间设置为数天。

我认为该方案并可行性是存在的,但是为了解决 jwt 的续签把整个流程改变了,为什么不考虑下 oauth2 的 password 模式和 client 模式呢?

  • 使用 redis 记录独立的过期时间
    那么和使用session存储也没有太多区别了

6. CSRF是什么

(Cross-site request forgery)跨站伪造请求。
攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的
常见攻击流程:

  • 受害者登录a.com,并保留了登录凭证(Cookie)。
  • 攻击者引诱受害者访问了b.com。
  • b.com向a.com发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的cookie
  • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
  • a.com以受害者的名义执行了act=xx。
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
CSRF的特点
  • 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
  • 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
  • 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
  • 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。

7. CSRF的解决方案

根据CSRF特点来处理,如果后端是restful服务,禁止跨域请求就可以杜绝绝大部分请求了,然后可以再加上同源检测(校验referer和origin)。
参考:如何防CSRF攻击

8. Reference

JWT入门
理解JWT的使用场景和优劣
如何防CSRF攻击