在JavaScript前端技术大行其道的今天,我们通常只需在后台构建API提供给前端调用,并且后端仅仅设计为给前端移动App调用。用户认证是Web应用的重要组成部分,基于API的用户认证有两个最佳解决方案 —— OAuth 2.0 和JWT(JSON Web Token)。
JWT的主要应用场景:
-
身份认证
在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 -
信息交换
在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
1、JWT定义及其组成
JWT(JSON Web Token)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
载荷(Payload)
我们先将用户认证的操作描述成一个JSON对象。其中添加了一些其他的信息,帮助今后收到这个JWT的服务器理解这个JWT。
{ "sub": "1", "iss": "http://localhost:8000/auth/login", "iat": 1451888119, "exp": 1454516119, "nbf": 1451888119, "jti": "37c107e4609ddbcc9c096ea5ee76c667" }
这里面的前6个字段都是由JWT的标准所定义的。
- sub: 该JWT所面向的用户
- iss: 该JWT的签发者
- iat(issued at): 在什么时候签发的token
- exp(expires): token什么时候过期
- nbf(not before):token在此时间之前不能被接收处理
- jti:JWT ID为web token提供唯一标识
这些定义都可以在标准中找到。
将上面的JSON对象进行base64编码可以得到下面的字符串:
eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ
这个字符串我们将它称作JWT的Payload(载荷)。
如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串:
var base64url = require('base64url') var header = { "from_user": "B", "target_user": "A" } console.log(base64url(JSON.stringify(header)))
注:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
头部(Header)
JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象:
{ "typ": "JWT", "alg": "HS256" }
在这里,我们说明了这是一个JWT,并且我们所用的签名算法(后面会提到)是HS256算法。
对它也要进行Base64编码,之后的字符串就成了JWT的Header(头部):
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
签名(签名)
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ
最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret):
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
这样就可以得到我们加密后的内容:
wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4
这一部分又叫做签名。
最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ.wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4
JTI和replay attack
重放攻击(Replay Attacks)又称重播攻击、回放攻击或新鲜性攻击(Freshness Attacks),是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。
jti是被放到JWT中的唯一的ID,可以防止重放攻击。
使用场景
最常见的使用场景是用作 身份认证(Authentication, Web 和 Mobile 皆可),还可以用作 组件间传递安全信息的载体。
组成
一个 JWT 由三个部分组成:Header, Playload, Signature:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 1. Headers // alg: 加密算法,比如 HMAC SHA256 或 RSA // typ: toke 的类型 { "alg": "HS256", "typ": "JWT" } // 2. Payload // 用户信息 // sub:常指用户 ID { "sub": "1234567890", "name": "John Doe", "admin": true } // 3. Signature // 服务端可以通过签名保证 JWT 并未被恶意修改,简单来说就是确认是否合法 // 如代码所示,利用 header 中声明的算法来生成,通常会在服务端指定一个 SECRET KEY,保证后期的安全校验 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), SECRET_KEY) |
最后,服务端通过 点号(.) 拼接三个部分生成一个可以发往客户端的 JWT:
1 2 3 |
base64UrlEncode(header) + '.' + base64UrlEncode(payload) + signature eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ |
如何使用JWT?
在身份鉴定的实现中,传统方法是在服务端存储一个session,给客户端返回一个cookie,而使用JWT之后,当用户使用它的认证信息登陆系统之后,会返回给用户一个JWT,用户只需要本地保存该token(通常使用local storage,也可以使用cookie)即可。
当用户希望访问一个受保护的路由或者资源的时候,通常应该在Authorization
头部使用Bearer
模式添加JWT,其内容看起来是下面这样:
Authorization: Bearer
因为用户的状态在服务端的内存中是不存储的,所以这是一种无状态的认证机制。服务端的保护路由将会检查请求头Authorization
中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此减少了需要查询数据库的需要。
JWT的这些特性使得我们可以完全依赖其无状态的特性提供数据API服务,甚至是创建一个下载流服务。因为JWT并不使用Cookie的,所以你可以使用任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)。
原理
官网的一张图很清晰地说明了整个交互的过程:
- 浏览器用用户名和密码发起登录请求
- 服务端验证后产生一个 JWT 返回
- 浏览器需要保存 token(HTML5 的 localStorage sessionStoreage 或者传统的 cookie),服务端不需要保存
- 浏览器在请求需要验证的资源时都需要带上 jwt,通常是在 HTTP header 中设置:
Authorization: Bearer
JWT 的优势
JWT 对比 基于 cookie 验证方式的优点(这篇文章写得很详细,以下简单罗列一下):
- Cross-domain / CORS: 可以对任何 server 发起 AJAX 请求 2. Stateless (a.k.a. Server side scalability): 服务端无需保存 token,扩展方便
- CDN: 可以把资源文件都放在 CDN 上,服务端其实只是提供 API
- Mobile ready: 移动端处理 jwt 比 cookie 容易多了
- CSRF: 没有 cookie
- Performance: 解密(HMACSHA256)比找session应该块多了
- Standard-based: RFC标准,语言库支持丰富,大厂(比如 Firebase, Google)使用
- Decoupling(架构去耦): 不需要绑定特定的身份验证方案
深入学习的一些资料
- 10 Things you should know about Tokens (亲切的中文翻译)
- Token based authentication in realtime frameworks like Socket.io
- JWT Authentication with AngularJS – Video and Tutorial
- 主要讲安全的 Cookies are bad for you: Improving web application security