JWT 入门指南
原文地址:https://blog.angular-university.io/angular-jwt/
这篇文章是两篇手把手教你如何在Angular
应用(也适用于企业级应用)中实现基于JWT
的认证方式的文章中的第一篇。
本篇文章的首要目标是详细学习JWTs
如何运行,包括它在web应用中怎么用来做用户认证和session
管理的。
第二篇文章中,我们会介绍基于JWT
的认证系统是在特定的Angular
应用中可以如何实现,但是这篇文章仅仅是介绍JWTs
。
为什么要深入了解JWT
全面详细了解JWT
是为了一下几点:
- 一个基于
JWT
实现的认证系统的解决方案 - 各种各样的使用差错技巧:了解错误信息、堆栈等
- 了解各种第三方的库和他们的文档
- 设计一个内部的认证系统的解决方案
- 选择和配置一个第三方的认证服务
在选择使用基于JWT
的认证解决方案之后,也会有涉及客户端一些代码的编写,当然服务端也需要。
这篇文档的最后,你会深入的理解JWT
最原始的加密技术,这些技术也会应用在其他安全场合。
你会知道为什么和什么时候会使用JWT
,你也会知道JWT
的格式,甚至知道如何排查签名的问题,并知晓一些在网上使用的小工具。
使用这些小工具你可以自己排查无数关于JWT
的问题。废话不多说,我们开始吧!
为什么使用JWTs
JWTs
相比于使用内存产生的随机token的用户session管理机制的最大优势在于,它可以把验证逻辑委托给第三方的服务,他们可能是如下几种:
- 集中式的自定义开发的认证服务器
- 典型的可以支持JWTs的商业产品,比如LDAP
- 完全外部的第三方认证提供商,比如 Autho
外部认证服务器可以完全和应用服务器分开,而且不必在网络上分享secret key
,也就是说我们的应用服务器没有意外丢失secret key
的可能。
另外,应用服务器和认证服务器无需保持连接也可以进行认证工作。
更好的一点是,应用服务器无需在内存中记住token,也就是说服务是无状态的。认证服务器可以产生token,传送给应用服务器,然后立刻删除。
还有一点是,应用服务器在数据库层面也无需存储口令的摘要信息。存储的东西越少,安全相关的丢失bug就相应减少了。
在这里你可以会想:对于内部的应用服务器,JWTs
是一个好的解决方案吗?当然,在这篇文章的最后一部分,我会讲关于JWTs
认证方面的一些典型企业级应用场景。
目录
在这篇文章中将会涵盖以下一个内容:
JWTs
是什么JWT
的一些网上校验工具jwt
的格式JWTs
的层次结构:头、载体、签名Base64Url
(对比Base64
)JWTs
的用户会话管理:主体和过期HS256
JWT
签名 - 是如何工作的- 数字签名
- 哈希方法和
SHA-256
RS256
JWT
签名 - 让我们聊聊公钥加密RS256
vsHS256
- 哪一个更好- JWKS(Json web key set)Endpoints
- 如何实现
JWT
周期循环签名 - 企业级
JWTs
- 总结和结论
JWTs是什么
JWT 仅仅是一个代用特殊信息的Json载体。JWTs的主要目的是为了确定它是否有效,而确定它是否有效我们仅仅只需要看它所携带的token。
我们不需要和第三方服务通信或是把JWTs保存在内存中,就可以保证它携带的信息是有效的--这是因为他们携带了信息认证码,即MAC。
JWT 由三部分组成:头、载体和签名。我们一个个讲,首先讲讲载体。
如何描述JWT 载体
JWT 的载体是一个 JavaScript 对象。这里有一个有效的载体对象:
{
"name":"john doe",
"email":"john@johndoe.com",
"admin": true
}
在这个例子中,载体包含了一个特定用户的身份信息,但是通常来说,载体可以是其他一些信息,比如一笔银行转账信息。
关于载体内容没有严格的约束,但是你一定要知道JWT是没有被加密的。所以拦截到token后,其中的任何信息都是可读的。
因此一定不要在载体中放入可以被攻击者直接获得的用户信息。
JWT Headers - 为什么他们必要
载体中的内容通过签名信息被接受者验证。但是签名是多种多样的,接受者首先需要知道要去找哪种类型的签名方式。
这个关于token类型的元信息被分开存储,它也是一个JavaScript
对象,和载体一起被发送出去。
这个分开存储的json对象即JWT的头,这里有一个有效的头示例:
{
"alg":"RS256",
"typ":"JWT"
}
从上面我们可以看到JWT header 也是一个简单的JavaScript对象,上面的签名算法是RS256。
我们后面再聊聊签名的类型,现在我们专注于了解签名在认证中的作用。
JWT 签名 - 认证怎么使用
上一部分讲的签名是消息认证码(MAC)。JWT 的签名只能从载体(加上头)和一个特定的秘钥产生。
下面是签名怎么保证认证的步骤:
- 用户提交用户名和口令到认证服务器,这个服务器可能是我们的服务器,更典型的应用时一个分开的服务
- 认证服务器校验通过后,产生token,这个token的载体中包含了用户的身份信息和过期时间
- 认证服务器用秘钥把token头和载体签名后传递给用户浏览器(接下来会详细讲如何使用签名)
- 浏览器携带签名后的JWT和HTTP请求一起发送给应用服务器
- 签名后的JWT是一个更有效的临时用户凭据,用于替代用户名密码这个永久用户凭证。
接下来,是应用服务器是怎么处理JWT token的:
- 应用服务器检查JWT签名,确认它持有秘钥的人对这个载体的签名
- 载体表示了特定的用户
- 只有认证服务器才有秘钥,并且认证服务器只给输入了正确密码的用户发送token
- 因此应用服务器可以确定token的持有者是经过认证的用户,也就是输入了正确密码的用户
- 应用服务器对这个用户处理请求
攻击者模拟用户访问应用服务器只有两种,要么是盗了用户的用户名和密码,要么是知道了认证服务器的私钥。
到这里我们可以看到,签名是JWT的关键部分。
签名是让服务器无状态处理用户HTTP请求的关键,他使得服务器只需要验证请求中携带的token即可,无需每次都携带用户名和密码。
JWTs的目的是无状态的服务吗
让服务无状态只是好的影响,JWTs的关键好处是认证服务器产生JWT和应用服务器验证JWT可以完全分离。
也就是说服务仅需要最小层度的认证逻辑即可--验证JWT。
这也让一个集群中的所有应用都可以把注册登录流程放在一个单独的认证服务器上。
这也使得应用服务器更加简单,也更加安全。因为大部分认证功能都在认证服务器,而且应用见可以共用。
现在我们清楚的知道了JWT是怎么让服务器无状态的,让我们来看看具体的实现细节吧!
JWT 是什么样子的
(这里有一个视频讲解JWT的三个组成部分)
接下来我们看看一个从网上验证工具<jwt.io>上的JWT的例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
你可能会想,Json对象哪里去了呢?不要急,我们接下来会讲。实际上在本篇末尾,你会深入了解这个奇怪字符串的方方面面。
我们可以看到这个字符串使用点号划分了3部分,第一个点号之前的就代表JWT头:
JWT Header:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
第二部分,两个点号之间的字符串是载体:
JWT Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
第二个点号后的部分就是签名部分:
JWT Signature:
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
如果你想自己确认信息是否是对的,你可以从JWT验证工具网站上复制过来,来比较一下。
那么,这些字符到底是什么呢?我们怎么从JWT上获取可读的信息用来排查问题呢?<jwt.io>是怎么得到原json对象的呢?
被包装的Base64,是不是就是Base64Url?
事实上,载体、头和签名都还是可读的格式。
我们只是想要确保在网络上传输的JWTtoken不会由于编码问题变成乱码(如“garbled text” qîüö:Ã
)。
乱码问题是由于世界上的计算机没有统一的处理字符串编码的方式,比如 UTF-8、ISO8859-1等。
这个乱码问题很普遍:无论什么时候我们从平台上获取一个字符串,都是用了一个编码的方式。及时我们没有指定编码方式:
- 要么是操作系统默认的编码方式
- 要么是服务端默认的配置参数
我们希望通过网络传输的字符不必考虑这些问题,这就要求我们选择的字符集被所有的编码方式处理都有统一的结果,这就是为什么Base64编码格式的要解决的问题。
Base64 vs Base64Url
但是我们上面看到的JWT并不是Base64编码的,而是Base64Url。
它和Base64只是有几个字符不一样,所以我们可以把JWT当成是url参数进行传输,这也就是一些第三方登录页面重定向到我们的网站的方式。
让我们看看上面提到的JWT载体:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
我们可以从这个网站上解码:
{"sub":"1234567890","name":"John Doe","admin":true}
我们就得到了Json载体!这就为我们排查问题提供了信息。我们确实使用Base64解码器,接下来会更加深入了解这个编码,现在我们总结一下:
总结:我们知道了JWT 头和载体的内容形式:他们就是两个JavaScript对象,然后被转化成json格式,最后被以Base64Url编码,并以点号分隔。
这种格式就是发送json数据的实用方式。
下面这个视频展示了一些代码--展示了怎么生成、验证JWT token,并且详细展示了头和载体部分:
在我们讲签名之前,我们看看我们认证的时候在载体中放了些什么。
JWTs用户会话管理:主体和过期
我们之前提到过,JWT 载体可以是任何信息,而不仅仅是身份信息。通常我们在使用JWT 做验证的时候,我们会放几个信息:
- 用户的身份信息
- 会话过期时间
下面是我们常用放入载体的信息:
{
"iss": "Identifier of our Authentication Server",
"iat": 1504699136,
"sub": "github|353454354354353453",
"exp": 1504699256
}
解释一下这些属性:
iss
代表认证实体,这里是我们的认证服务器iat
JWT创建时间(以秒为单位)sub
用户的唯一标识exp
token 过期时间
这就是被称为 Bearer Token(携带信息的token),它表明了:
认证服务器确认了携带这个token的用户是sub属性指定的对应用户,放行吧
在我们知道了JWT 载体的典型用户之后,我们来看看签名吧。
JWTs 中有各种签名,本文会探讨其中的两种:HS256 和 RS256。我们从HS256 开始吧。
HS256 数字签名 - 他是如何工作的
像大多数签名一样,HS256数字签名是基于特定类型的算法:加密哈希算法。
这听起来很吓人,但是这些概念值得一学:这个知识点从过去20年开始使用,并且在未来很长时间都会使用。很多使用的安全策略都围绕哈希算法展开,这在web应用中随处可见。
好消息是我们需要知道关于哈希的内容,可以通过几张图表讲清楚,这也就是我们接下来要展示的。
我们会分两步来讲:第一步,什么是哈希算法;第二步,哈希算法是怎么和密码结合来产生信息验证码(MAC,即数字签名)。
在本节最后,你会掌握自己生成一个HS256的签名token,通过网上小工具和npm 包即可。
什么是哈希算法
哈希算法是一种特殊的算法,它有几个特点。并且哈希算法有很多实用应用,比如数字签名。
下面我们将它的4个有趣的属性,以及他们是如何保证产生的签名是可验证的。
我们先来介绍 SHA-256
哈希特性:不可逆
我们可以这样来比喻哈希:你可以通过牛排来得到汉堡包,但是你不可能通过汉堡包来用来做汉堡的牛排:
也就是说:这个算法是不可逆的。
这也意味着我们把头和载体经过哈希之后,没有谁可只通过输出的字符串看出我们的头和载体的元数据。
你可以在这个网站上试试SHA-256算法:哈希计算器,就像这样:
3f306b76e92c8a8fbae88a3ef1c0f9b0a81fe3a953fa9320d5d0281b059887c3
但是这也意味着哈希不是加密:加密从定义上来说是可以还原,我们需要通过加密的信息获取原信息是什么。
哈希特性:可重复性
另一个重要的属性是哈希是可重复的,也就是说无论我执行多少次哈希,得到的结果都是一样的。
我们可以这样认为,给定一系列输入和一个输出,我们可以很轻松的知道输出是否正确,因为我们可以通过输入计算出来对应的输出。当然,前提是我们知道所有的输入信息。
哈希特性:唯一性
另一个特性是:对于不同的输入,我们总会得到不同的输出。
这意味着我们把载体和头经过哈希之后,我们都会得到同样的输出信息,而且不会有输入会得到相同的输出信息。
哈希特性:随机性
最后一个特性是,我们不可能通过算法,可以从输出得到哈希之前的原值。
如果我们想通过输出来得到输入,通过猜想输入来比较输出的方式呢?是不是可以一个个字符来修改,当输出一致的时候,我们就知道了输入是什么了。
但是这里有一个问题:
通过哈希算法,这样是不可能的。
因为在哈希算法中,仅仅改变输入的一个字符,输出就会有大约50%的部分会发生改变,所以不可能通过比较的方式来得到输入。
那么知道了这些特性之后,我们可能会想:哈希算法是怎么得到数字签名的呢?
难道攻击者不能只获取头和载体,而忽略签名吗?
任何人都可以使用SHA-256算法来得到同样的输出,然后把签名放在JWT的签名中,是吗?
如何通过哈希算法得到数字签名
上面那个问题是存在的,任何人都可以通过头和载体得到签名。
但是HS256
并不仅仅是这样:除了头、载体之外,哈希的过程还增加了一个秘钥,加在一起进行哈希。
哈希出来的结果就是SHA-256 HMAC 或 Hash-based MAC。HS256签名使用的算法就是HMAC-SHA256算法。
所以只有拥有头、载体和特定的密码的人才可以生成token。
也就是说上面哈希的结果就是一个有效的数字签名。
这是因为只有生成载体并通过密码生成的签名是有效的,没有其他人可以生成同样的哈希串。
即哈希串作为一个证明载体是有效的数字证明而存在。
之后,哈希串被加载信息之中进行传递,让接受者去验证信息是没有被篡改的。
JWTs也是这样做的:JWT的最后一部分就是经过SHA-256对头和载体进行哈希之后,然后经过Base64Url编码的字符串。
怎么验证JWT签名
接受者受到消息之后,接受者必须有同样的密码才能够对签名进行验证,用来确保载体是有效的。
由于hash的四个特性,接受者需要通过头、载体、密码一起来样子签名是正确的,使用他们进行哈希之后对签名进行比较,如果一致,则是有效的token。
也就是接受者必须要有同样的密码才能够有效的接受消息,而不是抛弃他。
这也就是数字签名和HMACs的原理。下面我们来实际看看。
验证SHA-256 JWT签名
我们先看看上面的JWT,然后把签名部分和第二个点号删除,像下面我们看到的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
现在你可以把字符串复制到这个网站,然后用secret
作为密码,我们就重新得到了JWT 签名!
其实我们得到的是Base64编码版本的签名,因为=
在最后,这和Base64Url相似,但是不会:
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=
等号在url里会被展示成%3D
,有一点麻烦,但是这也解释了为什么我们在Url参数中传输时需要Base64Url。
网上并没有一些可用的Base64Url转换器,但是我们可以通过命令行完成这一工作。为了真正的验证HS256签名,这里有一个npm 包实现了Base64Url,也实现了Base64编解码。
base64url npm 包的使用
让我们把经过base64编码的字符串转换一下,来准确验证签名,npm包使用如下:
mkdir quick-test && cd quick-test
npm init
npm install base64url
node
> const base64url = require('base64url');
> base64url.fromBase64("TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=")
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
最后我们转换成功了,和上面JWT 签名中的一模一样。
现在恭喜你,你真正了解并实践了HS256 JWT签名是怎么产生的,并可以用这些工具来一步步调试了。
为什么还需要有其他的签名算法
上面是一个JWT签名的验证例子,HS256
也仅仅是一个算法。但是还有很多其他的签名算法存在,比如最通常使用的RS256
。
那么他们有什么不同呢?我们之前介绍了HS256算法,主要是让我们简单的理解MAC code。而且你可以在很多生成环境的应用中被使用。
但是通常来讲,我们一般使用RS256 这种算法替代HS256。因为接下来要讲的RS256
算法有很多优势。
HS256算法的不足
加入秘钥很简单的话,HS256签名是可以被暴力破解的。这里可能涉及更多的关键技术。
基于哈希的签名算法相比其他类型会特别容易被暴力破解。
而且,实际上HS256的缺点是,产生JWT服务器和接受服务器之间需要提前约定一个密码,用于验证和认证。
不可行的秘钥更新
这意味着如果我们想要更新密码,我们需要让是分布式的,而且需要让网络上所有需要的节点都保存,这并不容易,且会造成系统下线。
如果系统被不同的团队维护或是被第三方组织托管,这更加不可行了。
token的产生和验证方式一样
归结起来,产生JWTs 和验证方式是一样的,也就是说使用HS256算法,网络上有密码的节点都可以产生和验证token。
这也加大了密码被盗的风险,因为每个需要信息的节点都需要密码。而且不是所有的应用都有同样的安全等级,攻击者可以容易的获取到密码。
一种可以避免风险的方式是,每一个应用创建一个共享的密码。但是我们将要学到一个新的签名算法,它解决了所有的问题,这也是现在JWT的默认签名算法:RS256
。
RS256 JWT签名算法
使用RS256算法,我们也要像之前一样产生一个MAC 码,目的是创建签名并且验证特定的JWT是有效的。
但是现在我们要把产生token和验证token分开,只有验证服务可以产生token,而应用服务器来验证token。
我们这么做的方式是,我们产生2个秘钥,而不是一个秘钥:
- 只有认证服务器有一个私钥,用来给JWTs加签
- 私钥可以用来加签,但是不可用来验签
- 应用服务器有一个公钥,用来验证JWTs
- 公钥可以用来验签,但是不可以加签
- 公钥可以不必保密,因为攻击者得到它也不能够伪造签名
RSA加密技术介绍
RS256签名用来一组特定的秘钥,被称为RSA秘钥。RSA是一种加解密算法,一个秘钥用于加密,一个秘钥用于解密。
我们要知道RSA不是哈希算法,因为从定义上看,加密后的信息是要被还原,得到最初加密前的数据。
RSA公钥类似如下:
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB
-----END PUBLIC KEY-----
这看起来很复杂,但是它仅仅是一个唯一的字串,可以被命令行工具像openssl产生,或者这个网上工具产生。
这个公钥是被公开的,攻击者是可以获取的。
对应于公钥,有一个特定的私钥与之对应:
-----BEGIN RSA PRIVATE KEY-----
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算法加密载体
我们尝试这样给JWT加签:获取头和载体,然后使用私钥进行加密,然后发送。
接受者接受JWT,然后使用公钥解密,验证结果。
如果解密成功了,并成功获取了JSON载体,是不是就可以认为token是认证服务器产生并加签后的,对吗?
确实是这样,它足够我们验证token的正确性。但是我们不这么做是因为一些实用性的原因。
RSA算法相比哈希算法来说比较慢。对于大的载体,性能是一个阻碍,这是唯一一个原因。
那么,让我们看看HS256算法是如何运用在RSA中的。
实用RSA 和 SHA-256算法加签
实际实用中,我们通过哈希算法把头和载体处理,如:SHA-256。
这一步很快,然后我们获得了一个代表了输入的唯一数据,它比实际数据要小很多。
然后我们使用私钥加密这个哈希后的数据,我们就得到了RS256 签名。
这时我们就可以把它放在token的第三部分,然后发送出去。
接受者怎么验证 RS256签名
接受者接受信息之后,按如下处理:
- 把头和载体进行 SHA256 哈希
- 然后使用公钥解密签名,获得签名哈希
- 比较1、2步的哈希串
如果是一致的,说明JWT确实是认证服务器创建的。
任何人都可以计算哈希,但是只有认证服务器可以使用私钥加签数据。
到这里你认为还有其他遗漏吗?我们先动手实践怎么验证RS256,稍后再看看这个问题。
手把手验证RS256 JWT签名
我们首先从<jwt.io>上获取一个经RS256签名的JWT:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE
我们可以看到这和HS256签名后的JWT没有什么明显的区别,但是它确实是经过RSA私钥加密后的字符串。
我们单独看看头和载体:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
我们只需要使用SHA256进行哈希,然后使用私钥进行加密即可。
加密后的结果就是JWT签名!我们一起使用Node的加密组件来验证吧!无需安装额外的组件,因为node已经自带了。
这个模块自带了RSA-SHA256函数和一些其他的签名函数可以用来产生签名。
首先,我们需要把私钥保存在一个文件中,起名private.key
.
然后在命令行中,我们运行一下这段代码:
const crypto = require('crypto');
const fs = require('fs');
const sign = crypto.createSign('RSA-SHA256');
// copy / paste here the header and the payload of your JWT only
sign.write('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9');
sign.end();
var privateKey = fs.readFileSync('./private.key');
console.log(sign.sign(privateKey, 'base64'));
如果你使用和上面不同的JWT token,你只需要复制前面两部分到write
方法里即可。
然后我们得到了下面的结果:
EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=
这和上面的JWT签名完全不一样呢!!但是等待,这里为什么会有斜杠、等号?这是不经过转义是不可能放在url中的。
原来是因为我们使用的是Base64编码,而不是Base64Url编码,我们来转换一下吧:
bash$ node
const base64url = require('base64url');
base64url.fromBase64("EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=");
这就和上面的RS256签名一模一样了。
这说明我们理解了RS256 JWT签名,如果需要,我们可以自己排查问题了。
总结一下,RS256 JWT 签名就是 使用RSA算法加密经过SHA256哈希后的头和载体。
那么我们知道RS256是如何工作的,它为啥比HS256要好呢?
RS256 vs HS256 - 为什么选择 RS256
使用RS256,攻击者可以轻松得到经过哈希后的头和载体。但是到这里想要重新得到签名,需要暴力破解RSA,这对于一个足够长度的键是不可能的,这里有说明。
但是这不是我们选择RS256的原因。
使用RS256,只有认证服务器有私钥,可以加签token,这大大降低了私钥丢失的风险,所以更加安全。
但是让我们选择RS256的最大原因是:便捷的更新秘钥
怎么实现秘钥更新
我们知道用来验证token的公钥是公开的,并且攻击者也不能用它来做什么。因为攻击者不需要校验一个token,而是想要伪造token。
所以,我们可以自主在一个服务上发布公钥。
应用服务只需要连接这个服务器来获取公钥,然后周期性的获取,以防变化,或是周期性的变化。
所以应用服务器和认证服务器不需要同时停机更新秘钥。
那么公钥是怎么发布的呢?这里有一种方式:
JWKS 端点
发布公钥的方式有很多种,但是这种会比较熟悉:JWKS是Json web key set的简称。
有很多简单的npm包可以消费端点并验证JWTs,我们可以在本主体的第二篇文章中看到。
有很多端点可以发布一些列的公钥,而不仅仅是一个。
如果你想知道端点是怎么样的,你可以看看这个在线例子,下面是我们通过HTTP GET 请求得到的例子:
{
"keys": [
{
"alg": "RS256",
"kty": "RSA",
"use": "sig",
"x5c": [
"MIIDJTCCAg2gAwIBAgIJUP6A/iwWqvedMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWFuZ3VsYXJ1bml2LXNlY3VyaXR5LWNvdXJzZS5hdXRoMC5jb20wHhcNMTcwODI1MTMxNjUzWhcNMzEwNTA0MTMxNjUzWjAwMS4wLAYDVQQDEyVhbmd1bGFydW5pdi1zZWN1cml0eS1jb3Vyc2UuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUvZ+4dkT2nTfCDIwyH9K0tH4qYMGcW/KDYeh+TjBdASUS9cd741C0XMvmVSYGRP0BOLeXeaQaSdKBi8uRWFbfdjwGuB3awvGmybJZ028OF6XsnKH9eh/TQ/8M/aJ/Ft3gBHJmSZCuJ0I3JYSBEUrpCkWjkS5LtyxeCPA+usFAfixPnU5L5lyacj3t+dwdFHdkbXKUPxdVwwkEwfhlW4GJ79hsGaGIxMq6PjJ//TKkGadZxBo8FObdKuy7XrrOvug4FAKe+3H4Y5ZDoZZm5X7D0ec4USjewH1PMDR0N+KUJQMRjVul9EKg3ygyYDPOWVGNh6VC01lZL2Qq244HdxRwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwgr0c0DYG5+GlZmPRFkg3+xMWizAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBACBV4AyYA3bTiYWZvLtYpJuikwArPFD0J5wtAh1zxIVl+XQlR+S3dfcBn+90J8A677lSu0t7Q7qsZdcsrj28BKh5QF1dAUQgZiGfV3Dfe4/P5wUaaUo5Y1wKgFiusqg/mQ+kM3D8XL/Wlpt3p804dbFnmnGRKAJnijsvM56YFSTVO0JhrKv7XeueyX9LpifAVUJh9zFsiYMSYCgBe3NIhIfi4RkpzEwvFIBwtDe2k9gwIrPFJpovZte5uvi1BQAAoVxMuv7yfMmH6D5DVrAkMBsTKXU1z3WdIKbrieiwSDIWg88RD5flreeTDaCzrlgfXyNybi4UTUshbeo6SdkRiGs="
],
"n": "wUvZ-4dkT2nTfCDIwyH9K0tH4qYMGcW_KDYeh-TjBdASUS9cd741C0XMvmVSYGRP0BOLeXeaQaSdKBi8uRWFbfdjwGuB3awvGmybJZ028OF6XsnKH9eh_TQ_8M_aJ_Ft3gBHJmSZCuJ0I3JYSBEUrpCkWjkS5LtyxeCPA-usFAfixPnU5L5lyacj3t-dwdFHdkbXKUPxdVwwkEwfhlW4GJ79hsGaGIxMq6PjJ__TKkGadZxBo8FObdKuy7XrrOvug4FAKe-3H4Y5ZDoZZm5X7D0ec4USjewH1PMDR0N-KUJQMRjVul9EKg3ygyYDPOWVGNh6VC01lZL2Qq244HdxRw",
"e": "AQAB",
"kid": "QzY0NjREMjkyQTI4RTU2RkE4MUJBRDExNzY1MUY1N0I4QjFCODlBOQ",
"x5t": "QzY0NjREMjkyQTI4RTU2RkE4MUJBRDExNzY1MUY1N0I4QjFCODlBOQ"
}
]
}
kid
属性是一个秘钥识别码,x5c
代表了一个特定的公钥。
这种方式最好的特点是他是标准的,所以我们只需要访问url,和一个消费JWKS的库,我们就可以后的验证JWTs的公钥,而不需要安装任何东西。
JWTs通常被用在公网和通用登录解决方案中,那么内部网络和私用应用怎么使用呢?
企业应用JWTs实战
JWTs 也可应用于企业中,作为一种典型前置认证设置方案,这被称为安全责任。
很多公司使用的前置认证方案中,应用服务是在一个私有的网络代理后,仅仅需要在HTTP头中指明用户即可。
HTTP头中的用户身份通常是被网络中的统一模块设置,这个统一的登录页被安装在代理服务器上用来管理用户会话。
会话过期后,服务器会阻止进入,并重定向到登录页面,验证。
验证通过后,应用服务器会对在HTTP 头中设置了用户标识的请求全部进行处理。
问题是:在这种设置之下,网络中的任何人都可以通过设置HTTP 头的方式模拟一个用户。
对于这种问题有解决方案,像在应用服务器端设置ip白名单或是使用客户端正数的方式,但是大多数公司没有实施这些措施。
一个更好前置认证方案
前置的认证是一个很好的方案,因为应用开发者不需要实现认证的功能,也就节省了时间,也减少了bug的产生。
那么在私有网络中,我们有更方便的方式既可以保证安全,又让应用服务器可以很便捷的开发吗
我们可以考虑一下JWT:我们放置token在HTTP 头中,而不是用户的标识。然后我们把用户的其他信息放在签名后的JWT的载体中。
应用服务器首先需要验证JWT,然后在获取用户信息:
- 如果签名通过,用户请求被处理
- 不同不通过,则可以拒绝处理
这样我们就让认证系统在私有网络中也可以很好的工作了。再也不必盲目的处理带有用户信息的HTTP头的任何请求,因为我们可以确保请求是认证通过的,而不是攻击者伪造的另一个用户。
总结
这篇文章中,我们全面的了解了JWTs是什么,以及怎么应用在认证系统中。JWTs仅仅是一个Json格式的信息载体,并且带有可以方便验证的签名。
我们也不需要把JWTs认证的东西在网络上进行传播,导致信息的泄漏。
另一个安全相关的建议是我们不在载体中把用户的敏感信息放进去,因为它不是加密的。
在下一篇文章中,我们学习怎么在Angular应用中实现JWTs。