了解如何使用JSON Web令牌(JWT)实现访问授权验证


JSON Web令牌(JWT)可以轻松地在服务(应用程序/站点的内部和外部)之间发送只读签名的 “ 声明 ” 。声明是任何一些你希望别人能够读取/验证的数据,但不可改变。

IETF的定义:
“ JSON Web Token (JWT)是一种紧凑的 URL安全 方式,用来表示在不同部分之间进行传输的声明,它可被编码 为 JSON对象 , 使用JJSON Web Signature (JWS)进行数字 签名 .“ 

要识别/验证应用程序中的人员身份,需要在页面(或API端点)的标头Header或网址中放置基于标准的令牌,以证明该用户已登录并允许其访问所需内容。
例: https://www.yoursite.com/private-content/?token=eyJ0eXAiOiJKV1Qi.eyJrZXkiOi.eUiabuiKv

JWT是一串“url safe”字符,用于编码信息,令牌有三个组件(以句点分隔)(此处显示为多行以便于阅读,但用作单个文本字符串)

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9           // header
.eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9      
// payload
.eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM  
// signature

1. Header
JWT的第一部分是一个简单JavaScript对象的编码字符串表示,它描述了令牌以及所使用的散列算法。
2.Payload
JWT的第二部分构成了令牌的核心。Payload有效载荷长度与您在JWT中存储的数据量成比例。一般的经验法则是:将最小值存储在JWT中。
3. Signature签名
JWT的第三部分也是最后一部分是基于Header(第一部分)和正文(第二部分)生成的签名,用于验证 JWT是否有效。

什么是“声明”?
声明是预定义的键及其值:

  • iss:令牌的发行者
  • exp:到期时间戳(拒绝已过期的令牌)。注意:如规范中所定义,这必须以秒为单位。
  • iat:JWT发布的时间。可用于确定JWT的年龄
  • nbf:“not before”是令牌变为活动状态的未来时间。
  • jti:JWT的唯一标识符。用于防止JWT被重复使用或重放。
  • sub:令牌的主题(很少使用)
  • aud:令牌的受众(也很少使用)

案例
一个简单的例子。(完整源代码位于/ example目录中):
https://jwt.herokuapp.com/

该服务器架构使用node.js http服务器,我们在/example/server.js中创建了4个端点:

  1. / home:主页(不是必需的,但我们的登录表单是。)
  2. / auth:验证访问者(如果失败则返回错误+登录表单)
  3. / private:我们的受限内容 - 需要登录(有效会话令牌)才能看到此页面
  4. / logout:使令牌无效并注销用户(防止重新使用旧令牌)

我们故意让server.js尽可能简单:
  • 可读性
  • 可维护性
  • 可测试性(所有辅助/处理程序方法单独测试)

帮助方法
所有辅助方法都保存在/example/lib/helpers.js中 。两个最有趣/相关的方法是(这里显示的简化版本):

// generate the JWT
function generateToken(req){
  var token = jwt.sign({
    auth:  'magic',
    agent: req.headers['user-agent'],
    exp:   Math.floor(new Date().getTime()/1000) + 7*24*60*60;
// Note: in seconds!
  }, secret);  
// secret is defined in the environment variable JWT_SECRET
  return token;
}

当用户认证时产生了JWT令牌(这随后被发送回客户端在授权头用于后续请求)。
以及:

// validate the token supplied in request header
function validate(req, res) {
  var token = req.headers.authorization;
  try {
    var decoded = jwt.verify(token, secret);
  } catch (e) {
    return authFail(res);
  }
  if(!decoded || decoded.auth !== 'magic') {
    return authFail(res);
  } else {
    return privado(res, token);
  }
}

以上是检查客户端提供的JWT是有效的?如果有效则向请求者显示私有(“privado”)内容,如果不是则呈现authFail 错误页面。
注意:是的,这两种方法都是同步的。但是,鉴于这些方法都不需要任何 I / O 或 网络请求,因此同步计算它们非常安全。

问题
问:如果我把JWT放在URL或Header中它是否安全?
不安全。除非你使用SSL / TLS来加密连接,明确地发送令牌始将是不安全的(令牌可以被截获并重新使用)。一个天真的 “ 缓解 ”方式是向令牌添加可验证的 “声明”,例如检查请求是否来自相同的浏览器(用户代理), IP地址或更高级的“ 浏览器指纹 ” http://programmers.stackexchange.com/a/122385
解决的办法是下面任一个:

  • 使用一次性令牌(在点击链接后就过期失效)
  • 不要使用需要高度安全性的url-tokens。(例如:不要向某人发送允许他们执行交易的链接)

url中一次性JWT令牌的用例是:
  • 帐户验证 - 当您在网站上注册后通过电子邮件向其发送链接时。 https://yoursite.co/account/verify?token=jwt.goes.here
  • 密码重置 - 确保重新设置密码的人员可以访问属于该帐户的电子邮件。 https://yoursite.co/account/reset-password?token=jwt.goes.here

这两者都是一次性令牌的好的使用方式(在点击之后到期)。

问:我们如何使会话无效?
使用应用的用户的设备(手机/平板电脑/笔记本电脑) 被盗。你如何使他们使用的令牌无效?
JWT背后的想法是令牌是无状态的, 它们可以由集群中的任何节点计算,并且在没有(或慢的)请求数据库的情况下进行验证。

将令牌存储在数据库中?
1. LevelDB!如果您的应用程序很小或者您不想运行Redis服务器,则可以通过使用LevelDB获得Redis的大部分好处:http//leveldb.org/
我们可以任意存储有效的DB令牌或者 我们可以存储无效令牌。这两个都需要往返DB以检查是否有效/无效。所以我们更喜欢存储所有令牌,并将令牌的 有效属性从true更新为false。
存储在LevelDB中的示例记录:

"GUID" : {
 
"auth" : "true",
 
"created" : "timestamp",
 
"uid" : "1234"
}

我们将通过其GUID查找此记录:
var db = require('level');
db.get(GUID, function(err, record){
  // pseudo-code
  if(record.auth){
   
// display private content
  } else {
   
// show error message
  }
});

请参阅:example / lib / helpers.js 验证详细信息的方法。

2. Redis
Redis是存储令牌的可扩展方式。

问:返回访问者(会话之间没有状态)
Cookie存储在客户端上,并在每次请求时由浏览器发送到服务器。如果此人关闭浏览器,则会保留Cookie,因此可以在停止的位置继续操作,而无需再次登录。但是,cookie将在与路径和发布域匹配的所有请求上发送,包括不需要的图像和css。
localStorage 提供了一种更好的机制,用于在浏览器会话期间和之间存储令牌。

1.基于浏览器的应用程序
存储JWT有两种选择:

  1. 使用localStorage在客户端存储JWT(意味着您需要记住在authorization标题中发送JWT以用于后续的http / ajax请求)
  2. 将您的JWT存储在cookie中(设置并忘记)

(我们显然更喜欢无cookie方法。但如果做得好,cookie仍然在现代网络应用程序中占有一席之地!)

2.程序(API)访问
访问您的API的其他服务必须将令牌存储在检索系统中(例如:Redis或SQLite用于移动应用程序)并在每个请求时发回令牌。

如何生成密钥?
由于JSON网络令牌(JWT)不使用非对称加密签名,你不能使用ssh-keygen生成使用密钥,您可以轻松使用强密码,例如: https://www.grc.com/passwords.htm。只要它很长且随机。碰撞的可能性(因此有人能够解码您编码的JSON)非常低。如果你将两个强密码(字符串)连接在一起,你将拥有一个128位的ASCII字符串。因此,碰撞的可能性小于宇宙中[url=http://en.wikipedia.org/wiki/Observable_universeMatter_content_.E2.80.94_number_of_atoms]的原子数[/url]。

要使用Node的加密库快速轻松地创建密钥,请运行此命令。

node -e "console.log(require('crypto').randomBytes(32).toString('hex'));"

换句话说,您可以使用RSA密钥,但你并不需要。
您需要记住的主要事项是:不要与不在核心的人(“ DevOps团队 ”)共享密钥,或者在提交给GitHub时突然发布密钥!