查看原文
其他

一步步带你了解前后端分离利器之JWT

2018-02-01 徐刘根 Java后端技术

一、HTTP的无状态性

HTTP 是无状态协议,它不对之前发送过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求处理。假设要求登录认证的 Web 页面本身无法进行状态的管理(不记录已登录的状态),那么每次跳转新页面不是要再次登录,就是要在每次请求报文中附加参数来管理登录状态。

不可否认,无状态协议当然也有它的优点。由于不必保存状态,自然可减少服务器的 CPU 及内存资源的消耗。从另一侧面来说,也正是因为 HTTP 协议本身是非常简单的,所以才会被应用在各种场景里。

二、Cookie 技术的引入

如果让服务器管理全部客户端状态则会成为负担,保留无状态协议这个特征的同时又要解决类似的矛盾问题,于是引入了 Cookie 技术。Cookie 技术通过在请求和响应报文中写入Cookie信息来控制客户端的状态。

Cookie会根据从服务器端发送的响应报文内的一个叫做Set-Cookie 的首部字段信息,通知客户端保存 Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入Cookie 值后发送出去。

1、没有 Cookie 信息状态下的请求(图片来源《图解HTTP》)

2、第 2 次以后(存有 Cookie 信息状态) 的请求(图片来源《图解HTTP》)

3、详细介绍Cookie 传输过程

服务器端发现客户端发送过来的 Cookie 后, 会去检查究竟是从哪一个客户端发来的连接请求, 然后对比服务器上的记录, 最后得到之前的状态信息。

三、基于表单的认证

目前用户的认证多半是基于表单的认证,基于表单的认证一般会使用 Cookie 来管理Session(Session会话,Session代表着服务器和客户端一次会话的过程,直到Session失效(服务端关闭)或者客户端关闭时结束)。基于表单认证本身是通过服务器端的 Web应用,将客户端发送过来的用户ID和密码与之前登录过的信息做匹配来进行认证的。

但鉴于 HTTP 是无状态协议, 之前已认证成功的用户状态无法通过协议层面保存下来。 即无法实现状态管理, 因此即使当该用户下一次继续访问,也无法区分他与其他的用户。于是我们会使用Cookie 来管理 Session,以弥补 HTTP 协议中不存在的状态管理功能。

简单的来说就是,用户在登录的时候,会在Web服务器中开辟一段内存空间Session用于保存用户的认证信息和其他信息,用户登录成功之后会通过Set-Cookie的首部字段信息,通知客户端保存Cookie,而这Cookie保存的就是服务器端Session的ID,下次请求的时候客户端会带上该Cookie向服务器端发送请求,服务器端进行校验,如果Session中保存的有该ID的Session就表示用户认证通过,否则失败!

四、Session存储位置以及集群情况下的问题

Session 是存储在Web服务器(例如:Tomcat)中的,并针对每个客户端(客户),通过SessionID来区别不同用户的。Session是以Cookie技术或URL重写实现,默认以Cookie技术实现,服务端会给这次会话创造一个JSESSIONID的Cookie值。

但是一个显著的问题就是,在集群模式下如果通过Nginx负载均衡的时候,如果有一个用户登录的时候请求被分配到服务器A上,登录成功后设置的Session就会存放在服务器A上了,但是在服务器B上却没有该用户的Session数据,当用户再次发起一个请求的时候,此时请求如果被分配到服务器B上,则就不会查询到该用户的登录状态,就会出现登录失败的情况!

一种可以想到的方式就是将多个Web服务器上存储的Session统一存储到某一存储介质中,保证进集群中的每一台机器都可以看到所有相同Session数据,这里的同步体现在所有的Session存储在同一的存储介质里边。

幸运的是我们常用的Tomcat容器已经为我们提供了一个接口,可以让我们实现将Session存储到除当前服务器之外的其他存储介质上,例如Redis等。

了解Spring Session的小伙伴可能都会知道Spring Session的本质就是通过实现Tomcat提供的该接口将Session存储到Redis中,以此来实现Session的统一存储管理,对Spring Session有兴趣的小伙伴可以参考往期的文章:

1、使用Redis存储Nginx+Tomcat负载均衡集群的Session

2、使用Spring Session和Redis解决分布式Session跨域共享问题

3、Spring Session解决分布式Session问题的实现原理

五、小结与需求痛点

Session和Cookie的目的相同,都是为了克服HTTP协议无状态的缺陷,但完成的方法不同。Session通过Cookie,在客户端保存SessionID,而将用户的其他会话消息保存在服务端的Session对象中,与此相对的,Cookie需要将所有信息都保存在客户端。因此Cookie存在着一定的安全隐患,例如本地Cookie中保存的用户名密码被破译,或Cookie被其他网站收集,例如:

  1. appA主动设置域B cookie,让域B cookie获取;

  2. XSS,在appA上通过JavaScript获取document.cookie,并传递给自己的appB。

上述过程我们简单的描述了Session的演进过程还有使用同步的方式解决Session在集群的时候出现的问题,但是我们意识到了使用Spring Session的方式来实现Session的同步是一件相对比较麻烦的事情,我们虽然使用Redis来进行同步,但是Redis并不是100%可靠的,我们需要对Redis搭建集群、进行主从同步复制、进行持久化等,显然这是一件很复杂的事情,因此有没有一种小而轻便的方式来实现我们的这种认证需求!那就是JWT了!

除了上述我们遇到的问题之外,在目前前后端分离的大环境下经常会遇到需要根据用户来分配权限和显示相对应信息的问题,虽然传统的Cookie和Session机制可以解决这个问题,但就通用性而言,JWT(JSON Web Token)相对来说更好。

看到这里很多小伙伴都已经按捺不住了!那JWT到底是什么呢?

六、JWT是什么

Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该标准被设计为紧凑且安全的,一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息。当然该标准也可直接被用于认证,也可被加密。

JWT的几个特点:

1、由于它们的尺寸较小,JWT可以通过URL,POST参数或HTTP头部发送。 另外,尺寸越小意味着传输速度越快。

2、有效载荷包含有关用户的所有必需信息,避免了多次查询数据库的需要。

JWT的使用场景:

1、验证

这是使用JWT最常见的情况。 一旦用户登录,每个后续请求将包括JWT。它将允许用户访问该令牌允许的路由,服务和资源。 单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,而且能够轻松地跨不同域使用。

2、信息交换

JWT是在各方之间安全传输信息的好方法, 因为JWT可以被签名(例如使用公钥/私钥对进行签名)。所以你可以确定发件人是他们说的那个人。 此外,由于使用头部(header)和有效载荷(payload)计算签名,因此您还可以验证内容是否未被篡改。

七、JWT的结构说明

JWT包含三个由点(.)分隔的部分,它们是:

  1. 头部(header)

  2. 有效负载(payload)

  3. 签名(signature)

因此,JWT通常看起来如下所示:

xxxxx.yyyyy.zzzzz

1、头部(header)

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

然后,将这个JSON用Base64编码,形成JWT的第一部分。

2、有效负载(payload)

令牌的第二部分是包含声明的有效载荷。 声明是关于实体(通常是用户)和附加元数据的声明。 有三种类型的声明:

  1. 标准中注册的声明;

  2. 公开声明;

  3. 私人声明;

(1)标准中注册的声明:这是一组预先定义的声明,这些声明不是强制性的,但建议提供一套有用的,可互操作的声明。 如下:

iss: jwt签发者

sub: jwt所面向的用户

aud: 接收jwt的一方

exp: jwt的过期时间,这个过期时间必须要大于签发时间

nbf: 定义在什么时间之前,该jwt都是不可用的.

iat: jwt的签发时间

jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

注意:声明名称只有三个字符长,因为JWT是紧凑的。

(2)公开声明:这些可以由使用JWT的人员随意定义。 但为避免冲突,应在IANA JSON Web令牌注册表中定义它们,或者将其定义为包含防冲突命名空间的URI。

(3)私人声明:这是为了共享使用它们的当事方之间共享信息而创建的声明,既不是登记声明,也不是公开声明。

示例如下:

然后将有效载荷进行Base64编码,以形成JSON Web令牌的第二部分。

3、签名(signature)

要创建签名部分,您必须采用头部(header),有效载荷(payload),密钥(secret),以及头部中指定的算法。例如,如果你想使用HMAC SHA256算法,签名将按以下方式创建:

签名通常用于验证JWT的发件人是谁,并JWT在传送的过程中不被篡改。

注意:上图红框中的secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。

4、案例演示

下面显示了一个登录请求成功之后服务端返回的Token,它由编码头部(header)、编码有效载荷(payload)和签名(signature)通过(.)拼接而成:

如果需要,你可以使用jwt.io的Debugger工具,来编码、验证和生成JWT。操作界面如下:

八、JWT的工作原理

在身份验证中,当用户使用他们的凭证(如用户名、密码)成功登录时,后台服务器将返回一个token,前端接收到这个token将其保存在本地(通常在本地存储中,也可以使用Cookie,但不是传统方法中创建会话,服务器并返回一个cookie)。下次用户想要访问受保护的路由或资源时,就将本地保存的token放在头部Header中发送到后台服务器。服务器接收到请求,检查头部中token的存在,如果存在就允许访问受保护的路由或资源,否则就不允许。如下所示:

一般默认的Value是以“Bearer ”开始,注意这里的Bearer之后有一个空格,以便后端进行分割。

这是一种无状态身份验证机制,因为用户状态永远不会保存在服务器内存中。 由于JWT是独立的,所有必要的信息都在那里,所以减少了多次查询数据库的需求。

九、总结

1、优点

(1)因为Json的通用性,所以JWT是可以进行跨语言支持的,像Java、JavaScript、NodeJS、PHP等很多语言都可以使用。

(2)因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。

(3)便于传输,JWT的构成非常简单,字节占用很小,所以它是非常便于传输的。

(4)它不需要在服务端保存会话信息, 所以它易于应用的扩展

2、安全相关

(1)不应该在JWT的payload部分存放敏感信息,因为该部分是客户端可解密的部分。

(2)保护好secret私钥,该私钥非常重要。

(3)如果可以,请使用HTTPS协议,不!是务必使用HTTPS!

十、文末彩蛋

后续会有两至三篇文章介绍JWT的使用和JWT的优缺点以及如何保证token的安全性等,敬请期待!

参考文章:

1、https://www.bysocket.com/?p=362 
2、
https://www.bysocket.com/?p=384 
3、
服务器前后端分离之JWT用户认证 
4、部分截图和内容参考《图解HTTP》

点击图片查看更多推荐内容

↓↓↓

这些开挂的Chrome插件助你的工作和学习事半功倍!

2018 年,去百度面试 Java 后端的一次面试经历!

目前最流行的开发模式DevOps究竟是什么鬼?


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存