JWT(Json web token)是目前最流行的跨域认证解决方案,本博客介绍它的原理。 JWT是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该标准被设计为紧凑且安全的,一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息。当然该标准也可直接被用于认证,也可被加密。
JWT 定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法
或者是 RSA
的公钥密钥对进行签名。
1、用户向服务器发送用户名和密码。 2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。 3、服务器向用户返回一个 session_id,写入用户的 Cookie。 4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。 5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。比如:A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,当访问另一个网站时会自动登录,该怎么实现?
- 一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
- 另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次-请求都发回服务器。JWT 就是这种方案的一个代表。
-
验证 JWT最常见的应用场景。 一旦用户登录,每个后续请求将携带JWT。它将允许用户访问该令牌允许的路由,服务和资源。
-
单点登录 当今广泛使用JWT的一项功能,因为它的开销很小,而且能够轻松地跨域。
-
信息交换 JWT是在各方之间安全传输信息的好方法, 因为JWT可以被签名(例如使用公钥/私钥对进行签名)。此外,由于使用头部(header)和有效载荷(payload)计算签名,因此还可以验证内容是否被篡改。
JWT 的原理是:服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{
"姓名": "梁云亮",
"角色": "管理员",
"到期时间": "2010年10月10日11点11分"
}
以后,用户与服务端通信的时候,都要发送这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。 这样服务器就不用保存任何 session 数据,也就是说,服务器变成无状态的了,从而比较容易实现扩展。
JWT结构说明JWT数据示例: JWT包含三个由点(.)分隔的部分,它们是:
- 头部(header)
- 有效负载(payload)
- 签名(signature) 因此,JWT通常看起来如下所示:
Header 部分是一个 JSON 对象,描述 JWT 的元数据,包含token 类型(即JWT)和采用的加密算法(如HMAC SHA256或RSA),通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。 最后,将这个JSON用Base64编码,形成JWT的第一部分。
Payload 负载Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据(比如用户ID)。 Payload 是包含声明的有效载荷。 声明是关于实体(通常是用户)和附加元数据的声明。 有三种类型的声明:
- 标准声明
- 公开声明
- 私人声明
标准声明规范里面预先定义了如下几个常用的声明(建议非强制):
- iss(issuer): jwt签发者
- sub(subject): 主题(jwt所面向的用户)
- aud(audience): 接收jwt的一方
- nbf(not before): 生效时间(定义在什么时间之前,该jwt都是不可用的)
- iat(issued at): jwt的签发时间
- exp(expiration time): jwt的过期时间,必须要大于签发时间
- jti(jwt id): jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
示例:
{
"iss": "lion1ou JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "lion1ou@163.com"
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。 需要将有效载荷进行Base64编码,以形成JSON Web令牌的第二部分。
Signature 签名Signature 部分是对前两部分的签名,防止数据篡改。签名通常用于验证JWT的发件人是谁,并JWT在传送的过程中不被篡改。
前面两部分都是使用 Base64 进行编码的,即前端可以解密知道JWT里面的信息。 Signature 需要使用编码后的 header 和 payload 以及我们提供的密钥,然后使用 header 中指定的签名算法(HS256)进行签名。 要创建签名部分,必须采用头部(header),有效载荷(payload),密钥(secret),以及头部中指定的算法。例如使用HMAC SHA256算法,签名将按以下方式创建:
注意:上图红框中的密钥secret是人工指定的,是保存在服务器端的。JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和JWT的验证,所以,它就是服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret,那就意味着客户端是可以自己签发jwt了。
签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。原因是对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
JWT特点1、简洁(Compact):尺寸较小,意味着传输速度越快。JWT可以通过URL,POST参数或HTTP头部发送。 2、自包含(Self-contained):有效载荷包含有关用户的所有必需信息,避免了多次查询数据库的需要。 3、因为Json的通用性,所以JWT是支持跨语言的,像Java、JavaScript、NodeJS、PHP等很多语言都可以使用。 4、因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。 5、它不需要在服务端保存会话信息, 所以它易于应用的扩展 6、JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。 7、不应该在JWT的payload部分存放敏感信息,因为该部分是客户端可解密的部分。 8、JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。 9、JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。 10、为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
使用原理客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage中。此后,客户端每次与服务器通信,都要带上这个 JWT。可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
Authorization: Bearer
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
- 前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同xxxxx.yyyyy.zzzzz的字符串。
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP Header中的Authorization位(解决XSS和XSRF问题)。
- 后端检查是否存在,如存在验证JWT的有效性。(例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己)。
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
Base64是一种是可逆的编码技术,这样我们的信息就有可能被暴露。所以,在JWT中,不应该在负载里面加入任何敏感的数据。比如传输的是用户的User ID,这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道用户的密码了。 因此,JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。
附 框架上面说了jwt是一种标准,所以现在也有很多框架给我们使用,比如在官网中看到 java有以下几个:auth0、jose4j、nimbus-jose、jjwt、fusionauth、vertx
参考:
- https://andaily.com/blog/?p=956
- https://github.com/monkeyk/MyOIDC/
Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。
单点登录Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.com,nv.taobao.com,nz.taobao.com,login.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。
Base64URLHeader 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。