「翻译」你应该知道这十件关于token的事

原文地址: 10 Things You Should Know about Tokens

几周前,我们发表了一篇_《在AngularJS框架下,单页面应用中的cookies和tokens》_的文章。显然这个话题很受欢迎,因此我们发表了第二篇文章_《实时通讯框架(如socket.io)如何基于token实现身份验证》_。出于对这个话题的强烈兴趣,我们决定在这篇文章中继续探索更多关于基于token实现身份认证的问题和实现细节。那我们开始吧:

  1. tokens需要被储存起来(在local/session storage或cookies里)
  2. tokens可以和cookies一样设置过期时间,但更容易掌控
  3. local/session storage无法跨域,所以需要一个标记cookie
  4. CORS请求会发送预检请求
  5. 当你需要使用流传送时,使用token来获取已签名的请求
  6. XSS比XSRF更好预防
  7. 每一个请求都会发送token,所以要注意它的大小
  8. 如果你要保存机密信息,对token加密
  9. JSON Web Tokens可以在OAuth中使用
  10. tokens不是万能的,根据你的验证需求谨慎使用

1. tokens需要被储存起来(在local/session storage或cookies里)

在单页面应用情景下使用token的问题上,有些人会有“刷新浏览器之后,tokens去哪儿了”的疑惑,答案很简单:你需要把token保存在某个地方,无论是session storage, local storage还是客户端的cookie里。当浏览器不支持session storage时,浏览器会把数据转存到cookies里。

也许你会有“但是如果我把token存在cookie里和以前的做法有什么区别?”的疑问。其实并不尽然,这里你其实是将cookies作为一个存储机制,而非验证机制(也就是说,cookie不会被浏览器用来验证用户的身份,因此也不会有XSRF攻击的潜在威胁)。

2. tokens可以和cookies一样设置过期时间,但更容易掌控

tokens也有过期时间(JSON Web Tokens中的exp属性),否则用户在登录过一次后就可以永久无限地通过身份验证了。cookies同样有过期时间。

但使用cookie时,我们通过不同的方法来控制cookie的使用时限:

  1. cookies可以在浏览器关闭后立即被销毁(session cookies)
  2. 此外你还可以设置服务器端检查(通常由你的web框架来替你完成),也可以设置绝对过期时间或有效时长
  3. cookies可以在过期时间前长期有效(浏览器关闭后不销毁)

在使用token时,当token过期了,你仅仅需要获取一个新的。你可以在端口刷新token,这样就可以:

  1. 激活旧的token
  2. 检查用户是否还存在,或者权限是否被激活,或者任何其他必要的操作
  3. 授权一个新的token,并更新它的过期时间

你甚至可以在token中储存最初的授权时间,实现14天免登陆等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.post('/refresh_token', function (req, res) {
// verify the existing token
var profile = jwt.verify(req.body.token, secret);

// if more than 14 days old, force login
if (profile.original_iat - new Date() > 14) { // iat == issued at
return res.send(401); // re-logging
}

// check if the user still exists or if authorization hasn't been revoked
if (!valid) return res.send(401); // re-logging

// issue a new token
var refreshed_token = jwt.sign(profile, secret, { expiresInMinutes: 60*5 });
res.json({ token: refreshed_token });
});

如果你需要重新激活tokens(长期有效的tokens是很有用的设置),你需要设置一些注册授权的操作来检测用户的tokens。

3. local/session storage无法跨域,所以需要一个标记cookie

如果你将一个cookie的域名设置为.yourdomain.com,那么从yourdomain.comapp.youdomain.com路径都可以获取到这个cookie,我们假设用户在你的购物网站上,就能很容易地检测到他/她已经在主页面上登录,从而重定向到移动端app.youdoumain.com

但另一方面,存储在local/session storage中的tokens无法跨域获取,就算是子域也不可以。那么我们该怎么办呢?

一个可行的方法是,当用户在app.domain.com上进行身份验证时,你可以生成一个token,同时还设置一个用于.youdomain.com的cookie。

1
2
3
4
5
6
7
$.post('/authenticate', function() {
// store token on local/session storage or cookie
....

// create a cookie signaling that user is logged in
$.cookie('loggedin', profile.name, '.yourdomain.com');
});

那么, 在domain.com上,你就能检测是否有用于这个域名的cookie,如果有,就可以重定向到app.yourdomain.com。token就可以在app的子域上使用,接下来就可以进行常规操作了(如果token仍然有效,就继续使用这个token,如果距离上次登录的时间已经超过了你设置的使用token的有效时长,那么久需要获取一个新的token)。

有时会有cookie仍然存在,但是token被删除或者有其他误操作的情况发生。如果出现这样的状况,用户需要重新登录。但需要强调的是,正如我们前面提到的,我们并没有将cookie作为验证机制去使用,而是仅仅将它作为一种存储机制,协助我们在跨域的情况下完成信息的存储。

4. CORS请求会发送预检请求

有人提出验证请求的头信息不满足简单请求的头信息要求,所以发送到特定地址的所有的请求都需要先发送预检请求。

1
2
3
4
5
6
7
8
9
10
OPTIONS https://api.foo.com/bar
GET https://api.foo.com/bar
Authorization: Bearer ....

OPTIONS https://api.foo.com/bar2
GET https://api.foo.com/bar2
Authorization: Bearer ....

GET https://api.foo.com/bar
Authorization: Bearer ....

但这种情况只在你设置了Content-Type: application/json的情况下会发生。尽管对于绝大多数情况下都有这一设置。

一个小的警告是,OPTIONS请求头信息本身并没有包含验证(Authorization),所以你的web框架在处理OPTION请求和后续请求的时候应该区别对待(注意:Microsoft IIS在处理这个情况时会有一些问题)。

5. 当你需要使用流传送时,使用token来获取已签名的请求

当使用cookie时,你可以很容易地触发文件下载并且对其内容进行流传送操作。但使用token时,因为它的请求是通过XHR实现的,你不能依赖它实现。解决方法是像AWS一样,生成一个签名的请求,Hawk Bewits就是一个很好的实现框架:

请求

1
2
POST /download-file/123
Authorization: Bearer...

响应

1
ticket=lahdoiasdhoiwdowijaksjdoaisdjoasidja

这个ticket是无状态的,而且基于地址URL来生成:域名+路径+请求+头信息+时间戳+HMAC,并且有过期时间。因此可以在有效期内,比如接下来的5分钟内,用于下载文件。

接下来你可以重定向到/download-file/123?ticket=lahdoiasdhoiwdowijaksjdoaisdjoasidja。服务器会检查ticke是否有效,从而执行接下来的操作。

6. XSS比XSRF更好预防

cookie可以在服务器端设置HttpOnly属性,从而使它只能在服务器端被获取,而不能通过JavaScript获取。这能够有效防范植入浏览器端的代码窃取cookie内容(XSS 跨站脚本攻击)。

由于token是存储在local/session storage或客户端的cookie中的,攻击者很容易获取token实现XSS跨站脚本攻击。这是一个很现实的考虑,基于此,你应该让你的token的有效期尽量短一些。

如果从防范攻击的层面去考虑,一个主要的隐患是XSRF。事实上,XSRF是最被误解的一种攻击,对于普通开发者来说,他们也许还没有意识到这个风险,所以很多应用都没有防范XSRF的机制。但不管怎么说,谁都明白注入代码是什么意思。简单来说,如果你允许在你的网站上植入一段代码,并且将它一同渲染出来,你就对XSS打开了大门。因此基于我们的经验,防范XSS比防范XSRF更容易。除此之外,防范XSRF并没有在每一个网页框架上实现。换句话说,防范XSS更容易因为大部分模板引擎已经默认对代码进行转义处理了。

7. 每一个请求都会发送token,所以要注意它的大小

每次你发送一个API请求时,你都需要在验证头信息中发送token。

1
2
GET /foo
Authorization: Bearer ...2kb token...

VS

1
2
GET /foo
connect.sid: ...20 bytes cookie...

取决于你在token中存储多少信息,它也可以变得很大。另一方面,session cookies只是一个标识符(如connect.sid, PHPSESSID),其余的内容都存储在服务器上(如果你只有一台服务器,那么它就在你的内存中,如果你有一个服务器集群,它就会在一个数据库里)。

现在,没什么能够阻止你利用token实现一个简单的机制了。token包含了所需的基本信息,同时在服务器端,你可以在每一次请求中丰富token信息。cookie也会这么做,只不过现在由你来决定增加哪些信息,你能在代码中完全掌控这一步骤。

1
2
GET /foo
Authorization: Bearer ……500 bytes token….

接着服务器:

1
2
3
4
5
6
7
8
9
app.use('/api',
// validate token first
expressJwt({secret: secret}),

// enrich req.user with more data from db
function(req, res, next) {
req.user.extra_data = get_from_db();
next();
});

值得注意的是,你也可以将session完全存储在cookie中(而不是只将cookie作为标识符)。有些web平台支持这一做法,有些则不行。比如,在node.js中,你也可以使用mozilla/node-client-sessions

8. 如果你要保存机密信息,对token加密

token签名能够防止它被篡改。TLS/SSL防止使用过程中用户收到攻击。但如果有效载荷中包括用户的敏感信息(如SSN或其他),你可以对他们加密。JWT和JWE规范,但是大部分库都没有支持JWE,因此最简单的方法就是利用AES-CBC加密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.post('/authenticate', function (req, res) {
// validate user

// encrypt profile
var encrypted = { token: encryptAesSha256('shhhh', JSON.stringify(profile)) };

// sing the token
var token = jwt.sign(encrypted, secret, { expiresInMinutes: 60*5 });

res.json({ token: token });
}

function encryptAesSha256 (password, textToEncrypt) {
var cipher = crypto.createCipher('aes-256-cbc', password);
var crypted = cipher.update(textToEncrypt, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}

当然,你也可以使用第7条中的方法,将机密信息保存在数据库中。

9. JSON Web Tokens可以在OAuth中使用

tokens经常与OAuth一起使用。OAuth 2是用于解决身份认证的授权协议。用户在获取他们的数据时,认证服务器会返回一个access_token用于让接口记住用户。

通常这些token是不透明的,他们被称为bearertokens,是通过某种形式的哈希表保存在服务器上的随机字符串,并配有过期时间。服务器请求数据(如关联用户的通讯录),用户同意。下一次使用这个API时,token被发送给服务器,服务器在哈希表中查询,根据当下情景(token有没有过期?token是否有权获取相关数据?)决定是否通过验证。

这类token和我们前面讨论的token最大的区别就是签名的token(如JWT)是”无状态的“。他们不需要保存在哈希表在,因此是一种更轻量级的方法。OAuth2没有要求access_token的格式,因此你可以返回一个包含权限和过期时间的JWT。

10. tokens不是万能的,根据你的验证需求谨慎使用

几年之前,我们帮一个大公司完成了一个基于token的架构项目。这个公司拥有超过10万名员工及大量的信息需要维护。他们希望有一个集中式的企业级的验证存储系统。想象一下“用户W可以获取在X国的Y医院中的Z床位的用户id和姓名”。对于这个密集度的认证系统,你能想象,无论从技术层面还是行政层面,都将很快变得无法管理。

  • token会变得非常大
  • 应用或接口都会变得非常复杂
  • 无论谁想要获取这些权限都需要花很长时间管理它们

我们最终将工作集中在信息架构方面,以保障创建正确的权限。

总结:抑制住将所有东西都放进token的想法,在使用token前做一些分析和大小预估。