前面简单介绍了JWT的基础只是和简单的小Demo,可是对于JWT的应用场景和优缺点掌握的还够,这些东西只有本身实践过才能搞清楚其中的细节。在网上看到一个大佬对这块讲的比较好,就转载过来一块儿学习下。
原文连接
这样形如 A.B.C 的字符串时能敏感地认出这是使用了 jwt。发了这两篇文章后,有很多读者在文末留言,表达了对 jwt 使用方式的一些疑惑,以及到底哪些场景适合使用 jwt。我并非 jwt 方面的专家,和很多读者同样,起初研究时我也存在相同疑惑,甚至在逐渐接触后产生了更大的疑惑,通过这段时间项目中的使用和一些本身思考,把我的的总结整理成此文。web
这些基础知识简单地介绍下,千万别搞混了三个概念。在 jwt 中刚好同时涉及了这三个概念,笔者用大白话来作下通俗的讲解(非严谨定义,供我的理解)redis
通常是编码解码是为了方便以字节的方式表示数据,便于存储和网络传输。整个 jwt 串会被置于 http 的 Header 或者 url 中,为了避免出现乱码解析错误等意外,编码是有必要的。在 jwt 中以 .
分割的三个部分都通过 base64 编码(secret 部分是否进行 base64 编码是可选的,header 和 payload 则是必须进行 base64 编码)。注意,编码的一个特色:编码和解码的整个过程是可逆的。得知编码方式后,整个 jwt 串即是明文了,随意找个网站验证下解码后的内容:算法
base64spring
因此注意一点,payload 是必定不可以携带敏感数据如密码等信息的。数据库
签名的目的主要是为了验证我是“我”。jwt 中经常使用的签名算法是 HS256,可能大多数人对这个签名算法不熟悉,但 md5,sha 这样的签名算法确定是为人熟知的,签名算法共同的特色是整个过程是不可逆的。因为签名以前的主体内容(header,payload)会携带在 jwt 字符串中,因此须要使用带有密钥(yuè)的签名算法,密钥是服务器和签发者共享的。header 部分和 payload 部分若是被篡改,因为篡改者不知道密钥是什么,也没法生成新的 signature 部分,服务端也就没法经过,在 jwt 中,消息体是透明的,使用签名能够保证消息不被篡改。api
前面转载的文章中,原做者将 HS256 称之为加密算法,不太严谨。跨域
加密是将明文信息改变为难以读取的密文内容,使之不可读。只有拥有解密方法的对象,经由解密过程,才能将密文还原为正常可读的内容。加密算法一般按照加密方式的不一样分为对称加密(如 AES)和非对称加密(如 RSA)。你可能会疑惑:“jwt 中哪儿涉及加密算法了?”,其实 jwt 的 第一部分(header) 中的 alg 参数即可以指定不一样的算法来生成第三部分(signature),大部分支持 jwt 的框架至少都内置 rsa 这种非对称加密方式。这里诞生了第一个疑问缓存
疑问:一提到 rsa,大多数人第一想到的是非对称加密算法,而 jwt 的第三部分明确的英文定义是 signature,这不是矛盾吗?安全
划重点!服务器
rsa 加密和 rsa 签名 是两个概念!(吓得我都换行了)
这两个用法很好理解:
既然是加密,天然是不但愿别人知道个人消息,只有我本身才能解密,因此公钥负责加密,私钥负责解密。这是大多数的使用场景,使用 rsa 来加密。
既然是签名,天然是但愿别人不能冒充我发消息,只有我才能发布签名,因此私钥负责签名,公钥负责验证。
因此,在客户端使用 rsa 算法生成 jwt 串时,是使用私钥来“加密”的,而公钥是公开的,谁均可以解密,内容也没法变动(篡改者没法得知私钥)。
因此,在 jwt 中并无纯粹的加密过程,而是使加密之虚,行签名之实。
来聊聊几个场景,注意,如下的几个场景不是都和jwt贴合。
一次性验证
好比用户注册后须要发一封邮件让其激活帐户,一般邮件中须要有一个连接,这个连接须要具有如下的特性:可以标识用户,该连接具备时效性(一般只容许几小时以内激活),不能被篡改以激活其余可能的帐户…这种场景就和 jwt 的特性很是贴近,jwt 的 payload 中固定的参数:iss 签发者和 exp 过时时间正是为其作准备的。
restful api 的无状态认证
使用 jwt 来作 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务端共享 secret;过时时间由服务端校验,客户端定时刷新;签名信息不可被修改…spring security oauth jwt 提供了一套完整的 jwt 认证体系,以笔者的经验来看:使用 oauth2 或 jwt 来作 restful api 的认证都没有大问题,oauth2 功能更多,支持的场景更丰富,后者实现简单。
使用 jwt 作单点登陆+会话管理(不推荐)
在《八幅漫画理解使用JSON Web Token设计单点登陆系统》一文中说起了使用 jwt 来完成单点登陆,本文接下来的内容主要就是围绕这一点来进行讨论。若是你正在考虑使用 jwt+cookie 代替 session+cookie ,我强力不推荐你这么作。
首先明确一点:使用 jwt 来设计单点登陆系统是一个不太严谨的说法。首先 cookie+jwt 的方案前提是非跨域的单点登陆(cookie 没法被自动携带至其余域名),其次单点登陆系统包含了不少技术细节,至少包含了身份认证和会话管理,这还不涉及到权限管理。若是以为比较抽象,不妨用传统的 session+cookie 单点登陆方案来作类比,一般咱们能够选择 spring security(身份认证和权限管理的安全框架)和 spring session(session 共享)来构建,而选择用 jwt 设计单点登陆系统须要解决不少传统方案中一样存在和本不存在的问题,如下一一详细罗列。
前面的文章下有很多人留言提到这个问题,我则认为这不是问题。传统的 session+cookie 方案,若是泄露了 sessionId,别人一样能够盗用你的身份。扬汤止沸不如釜底抽薪,不妨来追根溯源一下,什么场景会致使你的 jwt 泄露。
遵循以下的实践能够尽量保护你的 jwt 不被泄露:使用 https 加密你的应用,返回 jwt 给客户端时设置 httpOnly=true 而且使用 cookie 而不是 LocalStorage 存储 jwt,这样能够防止 XSS 攻击和 CSRF 攻击(对这两种攻击感兴趣的童鞋能够看下 spring security 中对他们的介绍CSRF,XSS)
你要是正在使用 jwt 访问一个接口,这个时候你的同事跑过来把你的 jwt 抄走了,这种泄露,恕在下无力
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 记录独立的过时时间
实际上个人项目中因为历史遗留问题,就是使用 jwt 来作登陆和会话管理的,为了解决续签问题,咱们在 redis 中单独会每一个 jwt 设置了过时时间,每次访问时刷新 jwt 的过时时间,若 jwt 不存在与 redis 中则认为过时。
tips:精确控制 redis 的过时时间不是件容易的事,能够参考我最近的一篇借助于 spring session 讲解 redis 过时时间的排坑记录。
一样改变了 jwt 的流程,不过嘛,世间安得两全法。我只能奉劝各位还未使用 jwt 作会话管理的朋友,尽可能仍是选用传统的 session+cookie 方案,有不少成熟的分布式 session 框架和安全框架供你开箱即用。
具体的对比不在此文介绍,就一位读者的留言回复下它的提问
这么长一个字符串,还不如我把数据存到数据库,给一个长的很难碰撞的key来映射,也就是专用token。
这位兄弟认为 jwt 太长了,是否是能够考虑使用和 oauth2 同样的 uuid 来映射。这里面天然是有问题的,jwt 不只仅是做为身份的认证(验证签名是否正确,签发者是否存在,有限期是否过时),还在其 payload 中存储着会话信息,这是 jwt 和 session 的最大区别,一个在客户端携带会话信息,一个在服务端存储会话信息。若是真的是要将 jwt 的信息置于在共享存储中,那再找不到任何使用 jwt 的意义了。
jwt 和 oauth2 均可以用于 restful 的认证,就我我的的使用经验来看,spring security oauth2 能够很好的使用多种认证模式:client 模式,password 模式,implicit 模式(authorization code 模式不算单纯的接口认证模式),也能够很方便的实现权限控制,什么样的 api 须要什么样的权限,什么样的资源须要什么样的 scope…而 jwt 我只用它来实现过身份认证,功能较为单一(多是我没发现更多用法)。
在 web 应用中,使用 jwt 代替 session 存在不小的风险,你至少得解决本文中说起的那些问题,绝大多数状况下,传统的 cookie-session 机制工做得更好。jwt 适合作简单的 restful api 认证,颁发一个固定有效期的 jwt,下降 jwt 暴露的风险,不要对 jwt 作服务端的状态管理,这样才能体现出 jwt 无状态的优点。
可能对 jwt 的使用场景还有一些地方未被我察觉,后续会研究下 spring security oauth jwt 的源码,不知到时会不会有新发现。
我的以为大佬写的很不错,有兴趣的话能够去关注一下,或点击上方的连接。