Skip to content

Latest commit

 

History

History
370 lines (249 loc) · 18.9 KB

README-zh_CN.md

File metadata and controls

370 lines (249 loc) · 18.9 KB

JWT logo wider

学习如何使用 JSON Web Tokens (JWT) 进行鉴权

dilbert fixed the internet

学习怎么使用 JSON Web Token (JWT) 来加密你的 Web 应用或者移动应用!

Build Status codecov.io codeclimate-maintainability Dependencies Status devDependencies Status contributions welcome HitCount

为什么?

JSON Web Tokens (JWTs) 使得在服务之间(包括在你 app 或者网站的内部和外部发送只读签名 的 “声明“ 变得很简单

声明是你想让某些人阅读校验但不能修改的任意字节的数据。

注意如果听起来很啰嗦,请不要担心,阅读 5 分钟之后一切都会变得清晰起来的!

是什么?

JSON Web Token(JWT)是一种紧凑的 URL 安全方式,用于表示在双方之间传输的声明。JWT 中的声明被编码为使用JSON Web 签名(JWS)进行数字签名的 JSON 对象。——IETF

通俗一点

为了在你的 app(web或者移动端)中辨识或授权用户,在 header 或者页面(或者 API)的 url 中放置一个基于标准的 token,它表明了这个用户已经登录并且被允许获取到他想要的内容。

示例:https://www.yoursite.com/private-content/?token=eyJ0eXAiOiJKV1Qi.eyJrZXkiOi.eUiabuiKv

注意:如果这对于你而言还不够“安全”,往下翻到“security”这一节。

JWT 看起来是什么样的?

Tokens 是一系列“url 安全”的字符所组成的字符串,它包含了编码后的信息。 Tokens 由三部分组成(用小数点分割),为了便于阅读,下面用三行来展示,但是实际使用时是一个单独的字符串。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9           // 头部
.eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9      // 载荷
.eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM   // 签名

1. 头部

JWT 的第一部分是一个编码的字符串,它表示一个简单的 JavaScript 对象,这个对象描述了 token 所使用的哈希算法。

2. 载荷

JWT 的第二部分是 token 的核心,负载的长度与你在 JWT 中所存储的数据长度有关。 通常所遵守的准则是:存储尽量少的必要的数据在 JWT 中。

3. 签名

第三部分是最后一部分,是根据头部(第一部分)和主体(第二部分)所计算出来的一个签名,会被用于校验 JWT 是否有效。

什么是“声明”?

Claims are the predefined keys and their values:

声明是预定义的一系列和它们所对应的

  • iss: token 的发行人。
  • exp: 到期时间戳(已过期的令牌会被拒绝)。注意:如规范中所定义,以秒为单位。
  • iat: token 的发行时间。可以用于判断 JWT 的发行时间长。
  • nbf: "not before" 是 JWT 被激活的某个未来时间(可以理解为生效时间)。
  • jti: JWT 的第一无二的标识(编号),用于防止 JWT 被重复使用或者重复产生。
  • sub: token 的主题(很少使用)。
  • aud: token 的受众(同样很少使用)。

详情阅读: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#RegisteredClaimName

示例 contributions welcome

让我们通过一个简单的示例来继续深入学习 JWT。 (全部源码都在 /example 目录下)

动手尝试: https://jwt.herokuapp.com/

可以在 Gitpod(需要通过 GitHub 授权登录) 上亲自动手尝试一下这个示例。

Open in Gitpod

服务器

通过使用 node.js 的 核心模块 http 服务器,我们在 /example/server.js 创建了四个接口:

  1. /home : 首页(不是必要的,但是我们的 login 表单放在这里)。
  2. /auth : 对游客进行授权 (如果授权失败会返回错误并且回到首页的 login 表单)。
  3. /private : 我们的受保护的内容 - 需要登录(有合法的会话 token)才能看到这个页面。
  4. /logout : 使 token 失效并且登出用户(防止重复使用旧的 token)。

我们已经有意地server.js 写得足够的简单了:

  • 可阅读
  • 可维护
  • 可测试(所有的 helper/handler 方法都已经被单独测试)

注意: 如果你可以让示例更简单,请提交 issue 一起讨论!

Helper 方法

所有 helper 类方法都保存在 /example/lib/helpers.js 两个最有意思或者说最相关的方法是(下面是简化版本):

// 构造 JWT 的方法。
function generateToken(req){
  return jwt.sign({
    auth:  'magic',
    agent: req.headers['user-agent'],
    exp:   Math.floor(new Date().getTime()/1000) + 7*24*60*60; // 注意:单位是秒!
  }, secret);  // secret 被定义在环境变量 JWT_SECRET 中
}

当用户进行授权的时候,这个方法会计算出我们的 JWT token,这个 token 随后会被放在 Authorization 响应头发送回客户端,用于后续的请求。

另外一个

// 校验请求头 Authorization 中的 token 是否有效。
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);
  }
}

Which checks the JWT supplied by the client is valid, shows private ("privado") content to the requestor if valid and renders the authFail error page if its not.

该方法会检查客户端提供的 JWT 是否有效,如果有效就会展示私有内容(通过 "privado" 方法)给请求者;如果校验失败,会通过authFail 方法渲染 错误页

注意: 这两个方法都是同步的。这两个方法都没有进行任何的 IO 操作或者网络请求,所以同步计算是安全的。

提示:如果你正在为你的 Hapi.js 应用寻找 全能的 JWT Auth Hapi.js 插件异步校验或验证) 请查看: https://github.com/dwyl/hapi-auth-jwt2

测试

你可能已经注意到了教程开头的 [![Build Status][travis-image]][travis-url] 这些铭牌,这是一个标志,作者不只是堆砌代码在一起。

对服务器路由和 helper 类方法的测试都放在 /example/test

  1. /example/test/functional.js - 测试我们在 /example/lib/helpers.js 中创建的所有 helper 类方法Test Coverage
  2. /example/test/integration.js - 模拟用户对服务器所发起的请求并测试服务器响应

阅读所有测试案例,如有不清楚的地方,可以告诉我们

注意:我们为 http req/res 对象写了一个基本的“mock”: /example/test/mock.js

如果还不懂 mock 或者很好奇,请阅读:When to Mock (by "Uncle Bob")


常见问题及解答

有问题吗? 马上提问! >> https://github.com/dwyl/learn-json-web-tokens/issues

Q: 我把 JWT 放在 URL 或者 Header安全的吗?

问得好!答案是:“”,除非你使用 SSL/TLS 加密你的连接(https),使用明文发送 Token 永远都是不安全的(token 可以被拦截并且被坏蛋重用)。一种比较笨拙简单的方法是添加校验声明到 token,比如检查请求是否来自于同一个浏览器(user-agent),添加IP 地址或者更先进的“browser fingerprints”…… http://programmers.stackexchange.com/a/122385

解决方案包括:

  • 使用一次性 token,在链接点击后即失效 或者
  • 在安全性要求较高的场景下不把 token 放在 url 中。 (比如:不把执行交易的链接发送给别人)

JWT 放在 url 中的使用场景:

  • 账户校验 - 当你把激活账户的链接通过 Email 发送给在你网站注册了的客户的时候。 https://yoursite.co/account/verify?token=jwt.goes.here
  • 密码重置 - 确保重置密码的人能够登录与账户有关的邮件。 https://yoursite.co/account/reset-password?token=jwt.goes.here

上面的案例都是使用一次性 token 的适用场景 (点击后就失效)。

Q: 怎么使会话失效?

如果使用你 app 的人的设备(手机/平板电脑/笔记本电脑)被盗了,那你应该如何使它们使用的 token 失效?

JWT 背后的思想是无状态,它们可以被集群中的任意节点计算出来并且验证,而不用对数据库发起任何请求。

把 token 存在数据库中?

LevelDB

如果你的应用规模比较,或者你不想运行一个 Redis 服务器,你可以通过使用 LevelDB:http://leveldb.org/ 来从 Redis 获取最大的好处。

我们可以把有效的 token 存储在数据库中,或者相反地把非法的 token 存储在数据库中。这两种方案都需要往返数据库以检查 token 是否有效。所以我们倾向于存储所有的 token 到数据库,并且把 token 的 valid 字段从 true 更新为 false,表示 token 已经过期。

存储在 LevelDB 中的示例:

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

我们将通过 GUID 来查找这条记录:

var db = require('level');
db.get(GUID, function(err, record){
  // pseudo-code
  if(record.auth){
    // 展示私有内容(通过了校验)
  } else {
    // 展示错误信息(校验未通过)
  }
});

通过查看 example/lib/helpers.js 中的 validate 方法获取更多详情。

Redis

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

如果你从未接触过 Redis,请阅读:

Redis Scales (provided you have the RAM): http://stackoverflow.com/questions/10478794/more-than-4-billion-key-value-pairs-in-redis

从现在开始学习 Redis! https://github.com/dwyl/learn-redis

Memcache?

Quick answer: 使用 Redis: http://stackoverflow.com/questions/10558465/memcache-vs-redis

Q: 返回游客(会话之间没有状态保存

Cookie 存储在客户端,并且每次请求时都会由浏览器发送到服务器。如果关闭了浏览器,则会保留 Cookie,因此可以在上次停止的地方继续操作而不必再次登录。但是 cookie 会在与路径和发布域匹配的所有请求上发送,包括不需要的图像和 css 请求。

localStorage 提供了一种更好的在浏览器会话之间存储 token 的机制。

基于浏览器的应用

有以下两种方式存储你的 JWT:

  1. 使用 localStorage 在客户端存储你的 JWT(意味着你需要把 JWT 放在 authorization header 中返回给客户端,以便后续 http/ajax 请求使用
  2. 把 JWT 存储在 cookie 中。

我们更倾向于第一种方法。但是如果使用得当的话,cookie 仍然可以在现代 web 应用中发挥他们的作用!

一些有用的网站

编程式(API)访问

其它服务访问你的 API 时必须把令牌存储在检索系统中(比如:移动应用的 Redis 或 SQLite)并且把 token 带入到每一个请求中。

如何生成密钥?

如果这个问题在其它地方被提到过的话我感到抱歉。用于计算 token 的私钥和 ssh-keygen 生成的私钥是一样的吗? ~最初由 @skota 提出问题,更多详细: dwyl/hapi-auth-jwt2/issues/48

因为 JSON Web Token(JWT)不要求使用非对称加密进行签名,所以不必使用 ssh-keygen 生成密钥。你可以简单地只使用一个强密码,例如:https://www.grc.com/passwords.htm 提供了足够长的复杂的随机的字符串。这样的话使用相同加密字符串的可能性(有人能够修改有效负载,添加或修改声明以及创建有效签名的可能性)非常低。如果你将两个强密码(字符串)连接在一起,你将拥有一个 128 位的 ASCII 字符串。因此,碰撞的可能性小于宇宙中的原子数

To quickly and easily create a secret key using Node's crypto library, run this command.

使用以下命令可以通过 Node 的 crypto 模块快速而简单地创建一个密钥。

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

换句话说,你可以使用一个 RSA 密钥,但是这不是必要的。

你需要记住的最重要的一件事就是:不要把这个密钥泄露给核心组(”DevOps Team“)成员之外的任何人或者意外地将它发布到了 GitHub!

哪个 Node.js 模块?

在 NPM 上搜索 ”JSON Web Token“:https://www.npmjs.com/search?q=json+web+token 会产生许多结果!

npm search for json web token

使用 Hapi.js 构建 Web 应用?

我们努力简化在 Hapi.js 应用程序中使用 JWT 的过程,在这个过程中我们写了这个模块:https://github.com/dwyl/hapi-auth-jwt2

其它 Node.js 项目的常用方法

我们强烈推荐 jsonwebtoken 这个模块,它由我们的朋友@auth0 (校验/鉴权领域的专家)编写:

另外一个非常棒的选择是: https://github.com/joaquimserafim/json-web-token 也是我们的朋友 @joaquimserafim 编写的。

必要的阅读(预习)

深入阅读(推荐) contributions welcome

感谢您和我们一起学习!

如果您认为这篇快速阅读很有帮助, 请在 GitHub 上给我们一颗星星(Star)并且转推分享给其他人:https://twitter.com/olizilla/status/626487231860080640

olizilla tweet