jwt详解

#### JWT是什么?

JSON Web Token (or JWT)只是一个包含某种意义数据的JSON串。它最重要的特性就是,为了确认它是否有效,我们只需要看JWT本身的内容,而不需要借助于第三方服务或者在多个请求之间将其保存在内存中-这是因为它本身携带了信息验证码MAC(Message Authentication Code)。

一个JWT包含3个部分:头部Header,数据Payload,签名Signature。让我们逐个来了解一下,先从Payload开始吧。

#### JWT Payload看起来是怎样的呢?

Payload只是一个普通的Javascript 对象。对于payload的内容,JWT是没有任何限制的,但必须注意的是,JWT是没有加密的。因此,任何放在token里面的信息,如果被截获了,对任何人别人是可读的。因此,我们不应该在Payload里面存放任何黑客可以利用的用户信息。

#### JWT Header – 为什么是必须的?

Payload的内容在接收者端是通过签名(Signature)来校验的。不过存在多种类型的签名,因此,接收者需要知道使用的是哪种类型的签名。

这种关于token本身的元数据信息存放在另外的Javascript对象里面,并随着Payload一起发送给客户。这个独立的对象就是一个JSON对象,叫JWT Header,它也是普通的Javascript对象,在这里面我们可以看到签名类型信息,比如RS256。

#### JWT signatures – 如何被使用来完成认证的?

JWT的最后一部分是签名,它也叫信息验证码MAC。签名只能由拥有Payload、Header和密钥的角色生成。

那签名是如何完成认证功能的呢,且看:

1. 用户向认证服务器提交用户名和密码,认证服务器也可以和应用服务器部署在一起,但往往是独立的居多;
2. 认证服务器校验用户名和密码组合,然后创建一个JWT token,token的Payload里面包含用户的身份信息,以及过期时间戳;
3. 认证服务器使用密钥对Header和Payload进行签名,然后发送给客户浏览器;
4. 浏览器获取到经过签名的JWT token,然后在之后的每个HTTP请求中附带着发送给应用服务器。经过签名的JWT就像一个临时的用户凭证,代替了用户名和密码组合,之后都是JWT token和应用服务器打交道了;
5. 应用服务器检查JWT签名,确认Payload确实是由密钥拥有者签过名的;
6. Payload身份信息代表了某个用户;
7. 只有认证服务器拥有私钥,并且认证服务器只把token发给提供了正确密码的用户;
8. 因此应用服务器可以认为这个token是由认证服务器颁发的也是安全的,因为该用户具有了正确的密码;
9. 应用服务器继续完成HTTP请求,并认为这些请求确实属于这个用户;

这样的话,黑客假扮合法用户的办法要么是盗到了用户名和密码组合,要么盗到了认证服务器上的签名私钥。

签名的确是JWT的关键部分!签名使得无状态的服务器只需要通过查看HTTP请求中的JWT token就能保证HTTP请求是来自某个用户,而不需要每次请求时都发送密码。

#### JWT的目标是让服务器无状态?

实际上,JWT真正的好处是让认证服务器和校验JWT token的应用服务器可以完全分开,而让服务器无状态化只是它的一个副作用罢了。这意味着应用服务器只需要最简单的认证逻辑-校验JWT!我们可以将整个应用集群的登录/注册委托给一个单独的认证服务器。这也意味着应用服务器更简单更安全,因为更多的认证功能集中部署在认证服务器,可以被跨应用使用。

#### JSON Web Token看起来是怎样的呢?

我们可以看到,这个JWT包含3部分,是由“.”号分开的

- **JWT Header:**

```text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
```

- **JWT Payload:**

```text
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
```

- **JWT Signature:**

```text
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
```

#### Base64 vs Base64Url

但是我们在JWT看到的并不是Base64,实际上是Base64Url,它和Base64类似,但有一些字符不一样,因此我们可以将JWT作为URL的参数在请求行中进行传递。

那个“=”在URL栏中会显示为“%3D”,会显得混乱,这也解释了在我们把JWT拼接到URL发送时,需要Base64Url的原因。我们看一下Payload部分:

```text
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
```

我们使用在线解码器来解析它,就得到了一个JSON对象,因此,我们可以得到这样的结论:JWT Header和Payload的内容是普通的javascript对象,转换成JSON并进行Base64Url编码,以“.”号隔开。

#### 基于JWT的用户会话管理: 主题和期限

之前有提到,JWT的Payload理论上可以存放任何内容,不一定是用户身份信息,只不过使用JWT作为认证是最常用的方式。Payload还有一些特定的属性来支持:

1. 用户身份
2. 会话过期

这里是Payload的几个最常用的标准属性:

· iss 代表生成token的实体,一般就是认证服务器

· iat 创建JWT的时间戳(in seconds since Epoch)

· sub 包含用户的身份信息

· exp token的过期时间戳

我们把这叫做Bearer Token,意思是:**应用服务器确认这个token的持有者是具有由sub属性表示的ID的用户,因此可以放行**

#### 签名Signature

对于JWT,签名方式有很多种,这里我们主要了解HS256和RS256

##### HS256 JWT数字签名 – 它是如何工作的?

和很多签名方法一样,HS256 数字签名基于一种特殊的函数:加密哈希函数。

##### 什么是哈希函数(Hashing function)?

哈希函数是一种特殊的函数:它在数字签名中有很多实际的使用案例。现在我们将谈论它四个有趣的属性,然后看看这些属性如何使得我们可以生成可校验的签名。这里我们将使用的哈希函数是:SHA-256。

- **哈希函数属性 1 – 不可逆性**

这就意味着我们把Header和Payload作用于这个函数后,没有人可以从函数输出的信息中取回Header和Payload的原始值。

使用在线的哈希计算器,我们可以看到SHA-256的一个输出值如下:

```text
3f306b76e92c8a8fbae88a3ef1c0f9b0a81fe3a953fa9320d5d0281b059887c3
```

同时,哈希并不是加密,加密在定义中是可逆的,我们总是需要从加密后的信息中得到原始信息。

- **哈希函数属性 2 – 可重复生成**

另外一个需要知道的是,哈希函数是可重复生成信息的,也就是如果我们输入同样的Header和Payload信息,每次得到的结果是完全一样的。这就意味着,给定输入组合和哈希输出值,我们总是可以校验该输出值(比如签名signature)的正确性,因为我们可以重新计算(我们有输入值的情况下)。

- **哈希函数属性 3 – 没有冲突**

还有一个属性是,如果我们提供不同的输入值,总是得到不同的唯一的输出值。这就意味着我们将哈希函数作用于某个Payload和Header之后,总是得到相同的结果,其它输入值组合不会得到和这一样的结果,因此,哈希函数的不同输出值就代表了输入值的不同。

- **哈希函数属性 4 – 不可预测性**

哈希函数的最后一个属性就是不可预测性,给定一个输出值,无法通过各种手段猜测到输入值。假设我们尝试从上面的输出值中找到生成它的Payload,我们只能猜测输入值然后对比输出值看看是否匹配。

哈希函数是怎样完成数字签名的呢?黑客是否可以拿着Header和Payload,而不管Signature呢?任何人都可以使用SHA-256哈希函数生成一个输出,然后附加到JWT的signature部分,对吧?

##### 怎样使用哈希函数生成签名?

这是正确的,任何人都可以使用哈希函数,然后输入Header和Payload来生成结果。但HS256签名不止这样,我们拿到Header、Payload外,还要加上一个密码,将这三个输入值一起哈希。输出结果是一个SHA-256 HMAC或者基于哈希的MAC。如果需要重复生成,则需要同时拥有Header、Payload和密码才可以。这也意味着,哈希函数的输出结果是一个数字签名,因为输出结果就表示了Payload是由拥有密码的角色生成并加签了的,没有其它方式可以生成这样的输出值了。

将哈希结果附加到消息上,是为了让接收者可以验证。哈希结果叫HMAC:Hash-Based Message Authentication Code,是数字签名的一种形式。这就是我们在JWT中所做的,JWT的第三部分是由Header、Payload通过SHA-256函数生成,并使用Base64Url进行编码。

##### 如何校验JWT签名?

当我们的服务接收到HS256签名的JWT时,我们需要使用同样的密码才能校验并确认token里面的Payload是否有效。为了验证签名,我们只需要将JWT Header和Payload以及密码通过哈希函数生成结果。如果是使用HS256函数,JWT的接收者需要拿到和发送者一样的密码值。如果我们得到的哈希结果和JWT第三部分的签名值是一致的,则说明有效,可以确认发送者确实和接收者拥有相同的密码值。

而数字签名和HMAC又是如何工作的呢?

##### 为什么需要其它的签名类型呢?

以上解释了JWT签名是如何应用于认证的,HS256只是一种具体的签名类型。其它的签名类型中,最常用的是:RS256。

有什么区别呢?我们介绍HS256只是为了更容易理解MAC码的概念, 你可能也会发现它在一些生产环境的应用中被使用。但是一般来说,使用RS256签名方式会更好,下一节我们将看到,RS256相对于HS256来说有诸多优势。

##### HS256签名方式的劣势

如果输入的密码相对弱的话,HS256可能会被暴力破解,基于密钥的技术都有这个问题。更甚的是,HS256要求JWT的生产者和消费者都预先拥有相同的密码。

- **不切实际的密码分发**

这意味着我们在修改密码后,需要把它分发并安装到所有需要它的网络节点。这不仅不方便,而且容易出错,还涉及到服务器间的协调和暂停服务问题。如果服务器是由另外的团队维护,比如第三方组织,这种方式就更不可行了。

- **Token的创建和校验没有分离**

创建和校验JWT的能力没有区分开,使用HS256时,网络的任何人都可以创建和校验token,因为他们都有密码。这就意味着密码可能会从更多的地方丢失或者受攻击,因为密码到处分发,而并不是每个应用都具有一样的安全保护机制。

弥补这问题的一个方法是,创建一个共享的密码给每一种类型的应用。不过,我们马上要学习新的签名方式,这个签名方式解决了以上所有的问题,并且目前所有基于JWT的方案都默认使用的,那就是RS256。

##### RS256 JWT签名

使用RS256我们同样需要生成一个MAC,其目的仍然是创建一个数字签名来证明一个JWT的有效性。只是在这种签名方式中就,我们将创建token和校验token的能力分开,只有认证服务器具备创建的能力,而应用服务器,具备校验的能力。

这样,我们需要创建两个密钥而不是一个:

1. 仍然需要一个私钥,不过这次它只能被认证服务器拥有,只用来签名JWT。
2. 私钥只能用来签名JWT,不能用来校验它。
3. 第二个密钥叫做公钥(public key),是应用服务器使用来校验JWT。
4. 公钥可以用来校验JWT,但不能用来给JWT签名。
5. 公钥一般不需要严密保管,因为即便黑客拿到了,也无法使用它来伪造签名。

##### RSA加密技术介绍

RS256使用一种特殊的密钥,叫RSA密钥。RSA是一种加解密密钥,使用一个密钥进行加密,然后用另外一个密钥解密

来看一下RSA公钥是怎样的:

—–BEGIN PUBLIC KEY—–

```text
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB
```

—–END PUBLIC KEY—–

这个公钥是公开发布的,因此黑客根本不需要猜测,他本来就可以拥有它。

但这里还有一个RSA私钥:

—–BEGIN RSA PRIVATE KEY—–

```text
MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQABAoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5CpuGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0KSu5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aPFaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==
```

—–END RSA PRIVATE KEY—–

黑客没有任何办法猜测私钥。而且,这两个密钥是相关的,一个密钥加密的内容只能由另外的密钥来解密。那我们又如何用它们生成签名呢?

##### 为什么不用RSA加密Payload就完了?

现在尝试着使用RSA来生成一个数字签名:

我们使用Header和Payload,然后使用私钥对其进行RSA加密,最后返回JWT。

接收者拿到JWT后,使用公钥解密,然后检查解密后的值。如果解密过程顺利并且其输出是一个JSON值,往往就意味着该JWT就是认证服务器创建并加密了的。

相比哈希函数,RSA加密过程比较慢。对于数据比较大的Payload来说,可能会是个问题。

那HS256签名方式在实际中又是如何使用RSA的呢?

##### 接收者是怎样检查RS256签名的?

接收者将:

1. 取出Header和Payload,然后使用SHA-256进行哈希。
2. 使用公钥解密数字签名,得到签名的哈希值。
3. 接收者将解密签名得到的哈希值和刚使用Header和Payload参与计算的哈希值进行比较。如果两个哈希值相等,则证明JWT确实是由认证服务器创建的。

任何人都可以计算哈希值,但只有认证服务器可以使用RSA私钥对其进行加密。

##### 如何进行密钥分发部署

还记得之前我们说过,用来校验token的公钥可以随意分发,黑客无法使用它来做任何有意义的事情。然而黑客并不是想校验token,他们只是想伪造它们。这就使得我们将公钥放置到受我们自己控制的服务器上成为可能。应用服务器连接到公钥放置的服务器获取公钥,然后定期检查公钥是否有变化。因此,在更新密钥时,应用服务器和认证服务器不需要同时暂停服务。那公钥又如何分发呢?下面是一种可行的格式。

- **JSON Web Key Set Endpoints**

有多种发布公钥的格式,但这里有一种较为熟悉:JWKS,全称Json Web Key Set。

如果你好奇这些endpoints 看起来是怎样的,可以看一下这个线上例子live example,下面这个是我们从HTT GET请求得到的回复:

Kid是密钥身份, x5c是某种公钥的表示法。这种格式的优点是其标准化,我们只需要知道endpoint的URL,和一个可以解析JWKS的库,就可以使用公钥来校验JWT了,而不需要安装到自己的服务器。

JWT常常使用在公共网站上,以及社交产品的登录方案中。对于内部系统,它是怎么被使用的呢?

#### JWT在企业中的应用

JWT同样适用于企业内部,替代经典的存在已知安全隐患的预身份验证设置(Pre-Authentication setup)方式。

预身份验证设置方式中,我们的应用服务器在私有网络的一个代理后面运行,然后从HTTP请求头中获取当前用户信息。代表用户身份的HTTP请求头通常由中心化的登录页面填充,同时中心化的节点也对用户session进行管理(以前是把登录的用户信息存放在session中)。

当session过期后,服务器将阻止对应用的访问,并要求用户重新登录认证。之后,它将所有请求转发到应用服务器并在HTTP请求头添加代表用户身份的信息

##### 传统的session跨域失效问题

理解的cookie与session的交互流程,我们就明白了session失效的原因,比如客户端访问A服务器的时候,生成的jsessionid的之为11111,但是当浏览器去调用B服务器的资源时,会携带这个jsessionid过去,发现B服务器上没有与之对应的session,这时B服务器又会生成一个新的session,并通过set-cookie方法把与该新的session对应的jsessionid(如222)设置到cookie中(自己的后端代码逻辑),这时候浏览器的jsessionid就变为了222,当浏览器再访问A服务器时,发现与222对应的没有session,这时候A服务器又会重新生成新的session

##### session跨域的解决方案

1、session 复制 每一台服务器上都保持一份相同的session (造成额外的存储开销和网络开销)

2、session 集中存储 :存储在db、 存储在缓存服务器 (redis)

**问题是这种设置方式,内网上的任何人都可以假扮成某个用户,只要设置同样的HTTP请求头**

对此也有一些解决方案,比如白名单列表,或者某种客户凭证。

- **更好的预身份验证设置方式**

预身份验证设置方式是一个好主意,毕竟这种方式可以使得应用开发者不需要实现认证逻辑,减少开发时间和潜在的安全问题。如果能有预身份验证设置方式的便捷,又没有安全方面的妥协,岂不美哉?如果我们考虑到JWT,则可以轻松做到。我们不像以往那样将用户名放到HTTP请求头,而是将HTTP请求头封装成一个JWT。我们将用户名放到Payload里面,再由认证服务器加签。

应用服务器不再从HTTP请求头获取用户名,而是首先校验JWT:

1. 如果签名是正确的,则用户认证通过,请求可以放行;
2. 否则,应用服务器简单的拒绝请求就好了;

这样的结果就是,即使在私有网络内,我们的认证功能也可以正常工作。我们再也不需要通过HTTP请求头来识别用户了,我们保证了HTTP请求头的有效性并且是由代理生成的,而不是某黑客试图以某个用户身份登录。

- **总结**

通过本文,我们对JWT是什么有了一个全面的了解,以及它是如何在认证中被使用的。JWT只是一个简单的JSON对象,并且易于验证、难于伪造。

此外,JWT并不是一定要用来做认证的,我们可以使用JWT在网络上发送各种数据。

另外一个和安全相关的使用JWT的情况是授权:我们可以在Payload里面放置用户的角色列表,比如只读用户、管理员等等,对用户在应用服务器上的行为进行限制。

你可能感兴趣的:(安全,java,前端)