我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农! 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。html
JWT 全称是 JSON Web Token,是目前很是流行的跨域认证解决方案,在单点登陆场景中常用到。前端
有些人以为它很是好用,用了它以后就不用在服务端借助 redis 实现认证过程了,可是,还有一部分人认为它生来就有缺陷,根本不能用。java
这是为何呢?git
传统的认证方式
从一个登陆场景提及
你平时用过那么多网站和 APP,其中有不少都是须要登陆的吧,那我们就选一个场景出来讲说。程序员
以一个电商系统为例,若是你想要下单,首先须要注册一个帐号,拥有了帐号以后,须要输入用户名(好比手机号或邮箱)、密码完成登陆过程。以后你在一段时间内再次进入系统,是不须要输入用户名和密码的,只有在连续长时间不登陆的状况下(例如一个月没登陆过)访问系统,才须要再次输入用户名和密码。github
对于那些使用频率很高的网站或应用,一般是很长时间都不须要输入密码的,以致于你在换了一台电脑或者一部手机以后,一些常用的网站或 APP 的密码都不记得了。web
早期的 Cookie-Session 认证方式
早期互联网以 web 为主,客户端是浏览器 ,因此 Cookie-Session 方式是早期最经常使用的认证方式,直到如今,一些 web 网站依然用这种方式作认证。redis
认证过程大体以下:算法
- 用户输入用户名、密码或者用短信验证码方式登陆系统;
- 服务端验证后,建立一个 Session 信息,而且将 SessionID 存到 cookie,发送回浏览器;
- 下次客户端再发起请求,自动带上 cookie 信息,服务端经过 cookie 获取 Session 信息进行校验;
可是为何说它是传统的认证方式,由于如今人手一部智能手机,不少人都不用电脑,平时都是使用手机上的各类 APP,好比淘宝、拼多多等。 在这种潮流之下,传统的 Cookie-Session 就遇到了一些问题: 一、首先,Cookie-Session 只能在 web 场景下使用,若是是 APP 呢,APP 可没有地方存 cookie。 如今的产品基本上都同时提供 web 端和 APP 两种使用方式,有点产品甚至只有 APP。数据库
二、退一万步说,你作的产品只支持 web,也要考虑跨域问题, 但Cookie 是不能跨域的。 拿天猫商城来讲,当你进入天猫商城后,会看到顶部有天猫超市、天猫国际、天猫会员这些菜单。而点击这些菜单都会进入不一样的域名,不一样的域名下的 cookie 都是不同的,你在 A 域名下是没办法拿到 B 域名的 cookie 的,即便是子域也不行。
三、若是是分布式服务,须要考虑 Session 同步问题。 如今的互联网网站和 APP 基本上都是分布式部署,也就是服务端不止一台机器。当某个用户在页面上进行登陆操做后,这个登陆动做一定是请求到了其中某一台服务器上。你的身份信息得保存下来吧,传统方式就是存 Session。
接下来,问题来了。你访问了几个页面,这时,有个请求通过负载均衡,路由到了另一台服务器(不是你登陆的那台)。当后台接到请求后,要检查用户身份信息和权限,因而接口开始从从 Session 中获取用户信息。可是,这台服务器不是当时登陆的那台,并没存你的 Session ,这样后台服务就认为你是一个非登陆的用户,也就不能给你返回数据了。
因此,为了不这种状况的发生,就要作 Session 同步。一台服务器接收到登陆请求后,在当前服务器保存 Session 后,也要向其余几个服务器同步。
四、cookie 存在 CSRF(跨站请求伪造)的风险。 跨站请求伪造,是一种挟制用户在当前已登陆的Web应用程序上执行非本意的操做的攻击方法。CSRF 利用的是网站对用户网页浏览器的信任。简单地说,是攻击者经过一些技术手段欺骗用户的浏览器去访问一个本身曾经认证过的网站并运行一些操做(好比购买商品)。因为浏览器曾经认证过,因此被访问的网站会认为是真正的用户发起的操做。 好比说我是一个黑客,我发现你常常访问的一个技术网站存在 CSRF 漏洞。发布文章支持 html 格式,进而我在 html 中加入一些危险内容,例如
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
假设 src 指向的地址是一个你平时用的购物网站的付款地址(固然只是举例,真正的攻击可没这么简单),若是你以前登陆过而且标识你身份信息的 cookie 已经保存下来了。当你刷到我发布的这篇文章的时候,img 标签一加载,这个 CSRF 攻击就会起做用,在你不知情的状况下向这个网站付款了。
Cookie-Session 改造版
因为传统的 Cookie-Session 认证存在诸多问题,那能够把上面的方案改造一下。 一、改造 Cookie 既然 Cookie 不能在 APP 等非浏览器中使用,那就不用 cookie 作客户端存储,改用其余方式。 改为什么呢? web 中可使用 local storage,APP 中使用客户端数据库,这样既能这样就实现了跨域,而且避免了 CSRF 。
二、服务端也不存 Session 了,把 Session 信息拿出来存到 Redis 等内存数据库中,这样即提升了速度,又避免了 Session 同步问题;
通过改造以后变成了以下的认证过程:
- 用户输入用户名、密码或者用短信验证码方式登陆系统;
- 服务端通过验证,将认证信息构造好的数据结构存储到 Redis 中,并将 key 值返回给客户端;
- 客户端拿到返回的 key,存储到 local storage 或本地数据库;
- 下次客户端再次请求,把 key 值附加到 header 或者 请求体中;
- 服务端根据获取的 key,到 Redis 中获取认证信息;
下面两张图分别演示了首次登陆和非首次登陆的过程。
通过一顿猛如虎的改造,解决了传统 Cookie-Session 方式存在的问题。这种改造须要开发者在项目中自行完成。改造起来确定是费时费力的,并且还有可能存在漏洞。
JWT 出场
这时,JWT 就能够上场了,JWT 就是一种Cookie-Session改造版的具体实现,让你省去本身造轮子的时间,JWT 还有个好处,那就是你能够不用在服务端存储认证信息(好比 token),彻底由客户端提供,服务端只要根据 JWT 自身提供的解密算法就能够验证用户合法性,并且这个过程是安全的。
若是你是刚接触 JWT,最有疑问的一点可能就是: JWT 为何能够彻底依靠客户端(好比浏览器端)就能实现认证功能,认证信息全都存在客户端,怎么保证安全性?
JWT 数据结构
JWT 最后的形式就是个字符串,它由头部、载荷与签名这三部分组成,中间以「.」分隔。像下面这样:
头部
头部以 JSON 格式表示,用于指明令牌类型和加密算法。形式以下,表示使用 JWT 格式,加密算法采用 HS256,这是最经常使用的算法,除此以外还有不少其余的。
{ "alg": "HS256", "typ": "JWT" }
对应上图的红色 header 部分,须要 Base64 编码。
载荷
用来存储服务器须要的数据,好比用户信息,例如姓名、性别、年龄等,要注意的是重要的机密信息最好不要放到这里,好比密码等。
{ "name": "古时的风筝", "introduce": "英俊潇洒" }
另外,JWT 还规定了 7 个字段供开发者选用。
- iss (issuer):签发人
- exp (expiration time):过时时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
这部分信息也是要用 Base64 编码的。
签名
签名有一个计算公式。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), Secret )
使用HMACSHA256
算法计算得出,这个方法有两个参数,前一个参数是 (base64 编码的头部 + base64 编码的载荷)用点号相连,后一个参数是自定义的字符串密钥,密钥不要暴露在客户端,近应该服务器知道。
使用方式
了解了 JWT 的结构和算法后,那怎么使用呢?假设我这儿有个网站。
一、在用户登陆网站的时候,须要输入用户名、密码或者短信验证的方式登陆,登陆请求到达服务端的时候,服务端对帐号、密码进行验证,而后计算出 JWT 字符串,返回给客户端。
二、客户端拿到这个 JWT 字符串后,存储到 cookie 或者 浏览器的 LocalStorage 中。
三、再次发送请求,好比请求用户设置页面的时候,在 HTTP 请求头中加入 JWT 字符串,或者直接放到请求主体中。
四、服务端拿到这串 JWT 字符串后,使用 base64的头部和 base64 的载荷部分,经过HMACSHA256
算法计算签名部分,比较计算结果和传来的签名部分是否一致,若是一致,说明这次请求没有问题,若是不一致,说明请求过时或者是非法请求。
怎么保证安全性的
保证安全性的关键就是 HMACSHA256
或者与它同类型的加密算法,由于加密过程是不可逆的,因此不能根据传到前端的 JWT 传反解到密钥信息。
另外,不一样的头部和载荷加密以后获得的签名都是不一样的,因此,若是有人改了载荷部分的信息,那最后加密出的结果确定就和改以前的不同的,因此,最后验证的结果就是不合法的请求。
别人拿到完整 JWT 还安全吗
假设载荷部分存储了权限级别相关的字段,强盗拿到 JWT 串后想要修改成更高权限的级别,上面刚说了,这种状况下是确定不会得逞的,由于加密出来的签名会不同,服务器可能很容易的判别出来。
那若是强盗拿到后不作更改,直接用呢,那就没有办法了,为了更大程度上防止被强盗盗取,应该使用 HTTPS 协议而不是 HTTP 协议,这样能够有效的防止一些中间劫持攻击行为。
有同窗就要说了,这一点也不安全啊,拿到 JWT 串就能够轻松模拟请求了。确实是这样,可是前提是你怎么样能拿到,除了上面说的中间劫持外,还有什么办法吗?
除非强盗直接拿了你的电脑,那这样的话,对不起,不光 JWT 不安全了,其余任何网站,任何认证方式都不安全。
虽然这样的状况不多,可是在使用 JWT 的时候仍然要注意合理的设置过时时间,不要太长。
一个问题
JWT 有个问题,致使不少开发团队放弃使用它,那就是一旦颁发一个 JWT 令牌,服务端就没办法废弃掉它,除非等到它自身过时。有不少应用默认只容许最新登陆的一个客户端正常使用,不容许多端登陆,JWT 就没办法作到,由于颁发了新令牌,可是老的令牌在过时前仍然可用。这种状况下,就须要服务端增长相应的逻辑。
经常使用的 JWT 库
JWT 官网列出了各类语言对应的库,其中 Java 的以下几个。
以 java-jwt
为例。
一、引入对应的 Maven 包。
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
二、在登陆时,调用 create
方法获得一个令牌,并返回给前端。
public static String create(){ try { Algorithm algorithm = Algorithm.HMAC256("secret"); String token = JWT.create() .withIssuer("auth0") .withSubject("subject") .withClaim("name","古时的风筝") .withClaim("introduce","英俊潇洒") .sign(algorithm); System.out.println(token); return token; } catch (JWTCreationException exception){ //Invalid Signing configuration / Couldn't convert Claims. throw exception; } }
三、登陆成功后,再次发起请求的时候将 token 放到 header 或者请求体中,服务端对 token 进行验证。
public static Boolean verify(String token){ try { Algorithm algorithm = Algorithm.HMAC256("secret"); JWTVerifier verifier = JWT.require(algorithm) .withIssuer("auth0") .build(); //Reusable verifier instance DecodedJWT jwt = verifier.verify(token); String payload = jwt.getPayload(); String name = jwt.getClaim("name").asString(); String introduce = jwt.getClaim("introduce").asString(); System.out.println(payload); System.out.println(name); System.out.println(introduce); return true; } catch (JWTVerificationException exception){ //Invalid signature/claims return false; } }
四、用 create 方法生成 token,并用 verify 方法验证一下。
public static void main(String[] args){ String token = create(); Boolean result = verify(token); System.out.println(result); }
获得下面的结果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiaW50cm9kdWNlIjoi6Iux5L-K5r2H5rSSIiwiaXNzIjoiYXV0aDAiLCJuYW1lIjoi5Y-k5pe255qE6aOO562dIn0.ooQ1K_XyljjHf34Nv5iJvg1MQgVe6jlphxv4eeFt8pA eyJzdWIiOiJzdWJqZWN0IiwiaW50cm9kdWNlIjoi6Iux5L-K5r2H5rSSIiwiaXNzIjoiYXV0aDAiLCJuYW1lIjoi5Y-k5pe255qE6aOO562dIn0 古时的风筝 英俊潇洒 true
使用 create 方法建立的 JWT 串能够经过验证。
而若是我将 JWT 串中的载荷部分,两个点号中间的部分修改一下,而后再调用 verify 方法验证,会出现 JWTVerificationException
异常,不能经过验证。
壮士且慢,先给点个赞吧,老是被白嫖,身体吃不消!
公众号「古时的风筝」,Java 开发者,全栈工程师,bug 杀手,擅长解决问题。 一个兼具深度与广度的程序员鼓励师,本打算写诗却写起了代码的田园码农!坚持原创干货输出,你可选择如今就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一块儿变优秀!