Bearer Token

说明

上一篇博客中我详细说明了如何在web客户端和REST服务端建立token通信机制,详见使用json web token。本篇文章中,我主要介绍Bearer Token的相关定义与使用方法。以及安全性问题的一些处理对策。

转载请注明出处:http://www.fed123.com/2014/08/13/2014_bare_token/

###来龙去脉
诸如Ember,Angular,Backbone之类的前端框架类库正随着更加精细的Web应用而日益壮大。正因如此,服务器端的组建也正正在从传统的任务中解脱,转而变的更像API。API使得传统的前端和后端的概念解耦。开发者可以脱离前端,独立的开发后端,在测试上获得更大的便利。这种途径也使得一个移动应用和网页应用可以使用相同的后端。

当使用一个API时,其中一个挑战就是认证(authentication)。在传统的web应用中,服务端成功的返回一个响应(response)依赖于两件事。一是,他通过一种存储机制保存了会话信息(Session)。每一个会话都有它独特的信息(id),常常是一个长的,随机化的字符串,它被用来让未来的请求(Request)检索信息。其次,包含在响应头(Header)里面的信息使客户端保存了一个Cookie。服务器自动的在每个子请求里面加上了会话ID,这使得服务器可以通过检索Session中的信息来辨别用户。这就是传统的web应用逃避HTTP面向无连接的方法(This is how traditional web applications get around the fact that HTTP is stateless)。

API应该被设计成无状态的(Stateless)。这意味着没有登陆,注销的方法,也没有sessions,API的设计者同样也不能依赖Cookie,因为不能保证这些request是由浏览器所发出的。自然,我们需要一个新的机制。这篇文章关注于JSON Web Tokens,简写为JWTs,一个可能的解决这个问题的机制。这篇文章利用Node的Express框架作为后端,以及Backbone作为前端。

###常用方法
第一个是使用在HTTP规范中所制定的Basic Auth, 它需要在在响应中设定一个验证身份的Header。客户端必须在每个子响应是附加它们的凭证(credenbtial),包括它的密码。如果这些凭证通过了,那么用户的信息就会被传递到服务端应用。

第二个方面有点类似,但是使用应用自己的验证机制。通常包括将发送的凭证与存储的凭证进行检查。和Basic Auth相比,这种需要在每次请求(call)中发送凭证。

第三种是OAuth(或者OAuth2)。为第三方的认证所设计,但是更难配置。至少在服务器端更难。

在使用中,并不会每次都让用户提交用户名和密码,通常的情况是客户端通过一些可靠信息和服务器交换取token,这个token作为客服端再次请求的权限钥匙。Token通常比密码更加长而且复杂。比如说,JWTs通常会长达150个字符。一旦获得了token,在每次调用API的时候都要附加上它。然后,这仍然比直接发送账户和密码更加安全,哪怕是HTTPS。
把token想象成一个安全的护照。你在一个安全的前台验证你的身份(通过你的用户名和密码),如果你成功验证了自己,你就可以取得这个。当你走进大楼的时候(试图从调用API获取资源),你会被要求验证你的护照,而不是在前台重新验证。

JWTs

JWTs是一份草案,尽管在本质上它是一个老生常谈的一种更加具体的认证授权的机制。一个JWT被周期(period)分成了三个部分。JWT是URL-safe的,意味着可以用来查询字符参数。(译者注:也就是可以脱离URL,不用考虑URL的信息)。

JWT的第一部分是对一个简单js对象的编码后的字符串,这个js对象是用来描述这个token类型以及使用的hash算法。下面的例子展示的是一个使用了HMAC SHA-256算法的JWT token。


1
2
3
4

{
“typ” : “JWT”,
“alg” : “HS256”
}

在加密之后,这个对象变成了一个字符串:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
JWT的第二部分是token的核心,这部分同样是对一个js对象的编码,包含了一些摘要信息。有一些是必须的,有一些是选择性的。实例如下:


1
2
3
4
5

{
“iss”: “joe”,
“exp”: 1300819380,
“http://example.com/is_root”: true
}

这个结构被称为JWT Claims Set。这个iss是issuer的简写,表明请求的实体,可以是发出请求的用户的信息。exp是expires的简写,是用来指定token的生命周期。(相关参数参看:the document)加密编码之后如下:


1

eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ

JWT的第三个部分,是JWT根据第一部分和第二部分的签名(Signature)。像这个样子:


1

dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

最后将上面的合并起来,JWT token如下:


1

eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

处理Tokens

我们将用JWT simple模块去处理token,它将使我们从钻研如何加密解密中解脱出来。如果你有兴趣,可以阅读这篇说明,或者读这个仓库的源码。
首先我们将使用下面的命令安装这个库。记住你可以在命令中加入–save,让其自动的让其加入到你的package.json文件里面。

npm install jwt-simple 

在你应用的初始环节,加入以下代码。这个代码引入了Express和JWT simple,而且创建了一个新的Express应用。最后一行设定了app的一个名为jwtTokenSecret的变量,其值为‘YOUR_SECRET_STRING’(记得把它换成别的)。

var express = require('express'); var jwt = require('jwt-simple'); var app = express();  app.set('jwtTokenSecret', 'YOUR_SECRET_STRING'); 

获取token

我们需要做的第一件事就是让客户端通过他们的账号密码交换token。这里有2种可能的方法在RESTful API里面。第一种是使用POST请求来通过验证,使服务端发送带有token的响应。除此之外,你可以使用GET请求,这需要他们使用参数提供凭证(指URL),或者更好的使用请求头。
这篇文章的目的是为了解释token验证的方法而不是基本的用户名/密码验证机制。所以我们假设我们已经通过请求得到了用户名和密码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

User.findOne({ username: username }, function(err, user) {
if (err) {
// user not found
return res.send(401);
}
if (!user) {
// incorrect username
return res.send(401);
}
if (!user.validPassword(password)) {
// incorrect password
return res.send(401);
}
// User has authenticated OK
res.send(200);
});

如果用户成功验证账号和密码,然后我们生成一个token,返回给用户。


1
2
3
4
5
6
7
8
9
10
11

var expires = moment().add(‘days’, 7).valueOf();
var token = jwt.encode({
iss: user.id,
exp: expires
}, app.get(‘jwtTokenSecret’));
res.json({
token : token,
expires: expires,
user: user.toJSON()
});

注意到jwt.encode()函数有2个参数。第一个就是一个需要加密的对象,第二个是一个加密的密钥。这个token是由我们之前提到的iss和exp组成的。注意到Moment.js被用来设置token将在7天之后失效。而res.json()方法用来传递这个JSON对象给客户端。

验证Token

客户端获取到token后,应该在每次向服务器请求数据时附带这个token,然后服务端验证token。
为了验证JWT,我们需要写出一些可以完成这些功能的中间件(Middleware):

  • 检查附上的token
  • 试图解密
  • 验证token的可用性
  • 如果token是合法的,检索里面用户的信息,以及附加到请求的对象上
    我们来写一个中间件的框架 

    
    
    1
    2
    3
    4
    5
    6
    7
    8
    
    
    // @file jwtauth.js
    var UserModel = require(‘../models/user’);
    var jwt = require(‘jwt-simple’);
    module.exports = function(req, res, next) {
    // code goes here
    };

为了获得最大的可扩展性,我们允许客户端使用一下3个方法附加我们的token:作为请求链接(query)的参数,作为主体的参数(body),和作为请求头(Header)的参数。对于最后一个,我们将使用Header x-access-token。

下面是我们的允许在中间件的代码,试图去检索token:


1

var token = (req.body && req.body.access_token) || (req.query && req.query.access_token) || req.headers[‘x-access-token’];

注意到他为了访问req.body,我们需要首先使用express.bodyParser()中间件(译者注,这个是Express 3.x的中间件)。
下一步,我们讲解析JWT:


1
2
3
4
5
6
7
8
9
10
11
12

if (token) {
try {
var decoded = jwt.decode(token, app.get(‘jwtTokenSecret’));
// handle token here
} catch (err) {
return next();
}
} else {
next();
}

如果解析的过程失败,那么JWT Simple组件将会抛出一段异常。如果异常发生了,或者没有token,我们将会调用next()来继续处理请求。这代表喆我们无法确定用户。如果一个合格的token合法并且被解码,我们应该得到2个属性,iss包含着用户ID以及exp包含token过期的时间戳。我们将首先处理后者,如果它过期了,我们就拒绝它:


1
2
3

if (decoded.exp <= Date.now()) {
res.end(‘Access token has expired’, 400);
}

如果token依旧合法,我们可以从中检索出用户信息,并且附加到请求对象里面去:


1
2
3

User.findOne({ _id: decoded.iss }, function(err, user) {
req.user = user;
});

最后,将这个中间件附加到路由里面:


1
2
3
4
5

var jwtauth = require(‘./jwtauth.js’);
app.get(‘/something’, [express.bodyParser(), jwtauth], function(req, res){
// do something
});

或者匹配一些路由


1

app.all(‘/api/*’, [express.bodyParser(), jwtauth]);

客户端请求

我们提供了一个简单的get端去获得一个远端的token。这非常直接了,所以我们不用纠结细节,就是发起一个请求,传递用户名和密码,如果请求成功了,我们就会得到一个包含着token的响应。

我们现在研究的是后续的请求。一个方法是通过JQuery的ajaxSetup()方法。这可以直接用来做Ajax请求,或者通过前端框架使用包装过的Ajax方法。比如,假设我们将我们的请求使用window.localStorage.setItem(‘token’, ‘the-long-access-token’);放在本地存储(Local Storage)里面,我们可以通过这种方法将token附加到请求头里面:


1
2
3
4
5
6
7
8
9

var token = window.localStorage.getItem(‘token’);
if (token) {
$.ajaxSetup({
headers: {
‘x-access-token’: token
}
});
}

很简单,但是这会劫持所有Ajax请求,如果这里有一个token在本地存储里面。它将会附加到一个名为x-access-token的Header里面。

bear token

关于bear token,参看 RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage , 目前国内各大网站都是用不同的token,也没说必须使用bear token,只有twitter明确说明的是使用bear token。

OAuth 2.0 (RFC 6749) 定义了 Client 如何取得 Access Token 的方法。Client 可以用 Access Token 以 Resource Owner 的名义来向 Resource Server 取得 Protected Resource ,例如我 (Resource Owner) 授权一個手机 App (Client) 以我 (Resource Owner) 的名义去 Facebook (Resource Server) 取得我的朋友名单 (Protected Resource)。OAuth 2.0 定义Access Token 是 Resource Server 用来认证的唯一方式,有了这个, Resource Server 就不需要再提供其他认证方式,例如账号密码。

然而在 RFC 6749 里面只定义抽象的概念,细节如 Access Token 格式、怎么传到 Resource Server ,以及 Access Token 无效时, Resource Server 怎么处理,都没有定义。所以在 RFC 6750 另外定义了 Bearer Token 的用法。Bearer Token 是一种 Access Token ,由 Authorization Server 在 Resource Owner 的允许下核发给 Client ,Resource Server 只要认在这个 Token 就可以认定 Client 已经获取 Resource Owner 的许可,不需要用密码学的方式来验证这个 Token 的真伪。关于Token 被偷走的安全性问题,另一篇再说。

Bearer Token 的格式


1

Bearer XXXXXXXX

其中 XXXXXXXX 的格式 b64token ,ABNF 的定義:


1

b64token = 1*( ALPHA / DIGIT / “-” / “.” / “_” / “~” / “+” / “/” ) *”=”

写成 Regular Expression 即是:


1

/[A-Za-z0-9/-/._~/+//]+=*/

谢谢!

转载请注明出处:http://www.fed123.com/2014/08/13/2014_bare_token/

欢迎关注皓眸学问公众号(扫描左侧二维码),每天好文、新技术!任何学习疑问或者工作问题都可以给我留言、互动。T_T 皓眸大前端开发学习 T_T

未经允许不得转载:前端学堂fed123(Frontend education) » Bearer Token

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册