在Express.js中,ETag是如何工作的?

77

Expressjs自动发送ETags。我想知道ETag是如何生成的。它是基于由get例程动态生成的内容,还是有办法通过不经过生成内容(从DB获取的动态内容)的过程即可传回相同的ETag。

也许可以使用一个中间件,它仅检查是否为有效会话ID并返回客户端给出的相同ETag,或者基于URL +会话ID生成唯一的ETag。这种情况下,在不经过整个数据库调用和所有其他操作的情况下,请求就已经结束了。这样,我需要知道客户端是否在进行304调用。

我可以使用到期标签。但是当会话结束时,如果有人打开URL,它就不应该允许。所以我认为ETag也应该基于会话ID。在这种动态内容的场景中,修改如何工作?它能够被使用吗?


1
请问您能否澄清一下您的第二和第三段?更详细地了解您的具体问题会很有帮助。 - stellarchariot
我已经读了你的问题五遍,并在下面为那些来到这里的人提供了关于Etag的详细答案。在你的特定情况下,我认为你混淆了四个概念:Etag、SessionId、Authentication和Authorization。我强烈建议不要将它们中的任何一个用于其他用途。并且请记住,“过早优化是万恶之源!”--https://softwareengineering.stackexchange.com/q/80084 - Aidin
2个回答

120
让我在2021年为你解释一下,同时提供更新的信息和代码链接。
这是一个相对直接简单的概念,但作为开发者,你应该真正了解它,因为它可能会给你带来麻烦!
什么是Etag?
根据Wikipedia/Etag,Etag是一个HTTP头部。它可以在某些GET调用的DevTools中的“响应标头”部分中看到,如下面的截图所示。

enter image description here

在Express中,ETag可以以W/(弱,缺省)开头或不带(强),然后是<LEN>-<VALUE>,其中VALUE长度为27个字符,并且LEN是VALUE的十六进制长度。(2021年6月源代码

ETag的目的是什么?

啊,好问题。答案是:缓存!

(PS. 只缓存客户端和服务器之间的网络流量。也就是响应数据通过HTTP(S)发送到客户端的传输;而不是任何服务器到数据库的内部缓存等。)

如何缓存?

机制相对简单。

假设客户端(如Chrome浏览器)调用https://myserver.com/user/profile/get终端点并获得当前用户所有配置文件数据的大型JSON响应(例如,30个字段的名称、电话、照片URL等)。除了向您的应用程序交付响应作为JSON对象之外,客户端还将在其自己的私有内部网络层中将此数据存储在客户端侧缓存中:{'https://myserver.com/users/profile/get': <this-json-response-object> }

现在,下一次(甚至几天和会话后)客户端即将调用相同终端点.../user/profile/get时,它可以告诉服务器:“嘿,我在我的缓存中有这个<previous_json_from_the_cache>,如果你要发送的内容与此完全相同,请不要发送它。”

很酷,但这不是效率低下吗?

是的!

问题在于,如果客户端将整个JSON对象从缓存中发送到服务器请求中,这既是安全风险,也是相当低效的——同样的30个字段JSON对象可能会被发送两次以上。

这里发生的是,客户端(即Chrome浏览器)可以计算哈希值(例如MD5,既不可逆,又更短),并在第二个请求中说:“嘿,如果你要发送给我回来的JSON的MD5哈希值是这个<computed_hash>,我已经有它了!所以不用再发送一次。”

现在,服务器将像以前一样计算响应(从数据库中提取所有数据)。但是,在仅在发送响应数据之前,它会在服务器端计算响应的哈希值,以查看是否与客户端所说的匹配。如果是,则发送304 HTTP状态响应代码,而不是200,这意味着“没有任何更改。”

好的!就是这个吗?

嗯,在上面的例子中,如果你仔细观察,哈希计算是在客户端和服务器端都发生的。这会使更改算法变得困难。所以,实际上,“响应的哈希值”只在第一次由服务器端计算,并将返回给客户端。

这个“当前响应”的计算哈希值会随着响应一起返回,它在响应ETag头中。

因此,每当客户端接收到响应时,它会在内部缓存中存储:{".../profile/get": [<ETag>, <JSON-Response-Data>]}

然后,在任何未来的请求中,客户端将向服务器发送此ETag值(在某些标头中,如if-none-match),以表示如果新调用的响应将具有与此相同的ETag,则可以接收304。

因此,简要回顾一下:

  • ETag值只是响应数据(正文)的不可逆、短且快速的哈希值。
  • 服务器在响应中发送ETag头到客户端。
  • 客户端在请求中发送if-none-matched头(其值是之前从服务器接收到的Etag值)到服务器。

太好了!我该如何使用它?

默认情况下,它是在Express.js中进行的。所以,请坐享其成!

很少有情况需要您更改其设置。

什么情况下我不应该使用Etag?

啊!欢迎来到我的世界:D 这就是我来到这里并进行所有这些研究的方式。

Express uses etag package(只有一个文件,由同一批人管理)生成ETag值。在内部,etag使用sha1加密body,而没有其他奇怪的东西,以保持最佳性能。(如果您想象一下,此函数将被频繁调用!服务器接收和处理的每个GET调用平均至少一次或两次。)

为了确定它是否应该执行304还是200,当客户端说“我已经在我的缓存中有这些值了”时,Express使用fresh package(再次只有一个文件,实际上只有一个返回布尔值的函数,由同一批人维护)。在内部,fresh包读取请求头的if-none-matched标记(reqHeaders ['if-none-match'])并compares它与即将发送的响应的etagresHeaders ['etag'])。

很棒,那么问题在哪里?

当您的架构和客户端与服务器之间的通信依赖于自定义标头时,问题就会出现!

例如,您想要在任何请求上更新auth或session token,并在某些请求的RESPONSE HEADER上在后台刷新并发送新令牌。

目前EXPRESS的ETag实现仅依赖于响应体,而不是响应头。即使是他们允许放置的自定义函数(doccode),也只取响应体内容,而不是响应头。

因此,当响应(例如个人资料数据)未更改时,您的客户端可能会重用过时的身份验证令牌,并由于无效的身份验证/会话标记将用户踢出!

如何禁用它?

您可以执行app.set("etag", false);以停止Express发送它。 根据this answer,您还可以/应该通过app.use(nocache())使用nocache向客户端从服务器发送“嘿,客户端,永远不要自己缓存它!”头文件。

干杯!

PS. 最后说明:

  • 如果您考虑一下,ETags对于资产非常有价值(当响应数据大小达到100KB或更多时),但对于普通API端点数据则不是。 因此,为小型响应端点禁用它可能不是一个坏主意-事实上,这可能不值得支付开销。

4
这是一个很棒的答案。感谢你写出了所有的细节。非常有帮助。我也想知道,后端开发者是否可以覆盖缓存,比如说只针对一个响应,而不是禁用 Express 的全部缓存 / Etag。 - gimp3695
@gimp3695 是的,你可以这样做。如果是Node.js,那么你可以为其设置单独的路由,并在响应中手动设置。 - Aidin
1
非常详细,非常有用的答案。谢谢@Aidin - Omar Abu Saada
2
我在SO上读过的最好的答案之一。干杯! - era5tone
1
很棒的回答!不过应该提到一种正确的禁用ETag的方法,因为现在你只能覆盖它的值(在中间件中设置新的ETag头值)。如果你运行 - app.set('etag', false); - 或者 - app.disable('etag'); - 图像/ CSS 仍然会有 ETag。 - b4rtekb
当您在304中发送自定义标头时,浏览器将更新其本地缓存中标头的值。但是,如果您在第一个响应(返回200 OK并被缓存的响应)中发送标头,但在第二个响应(304响应)中根本不返回标头,则旧值将被重用。 请参见:https://httpwg.org/specs/rfc7234.html#freshening.responses “使用304(未修改)响应中提供的其他标头字段来替换存储响应中相应标头字段的所有实例。” - Kicsi

59

在撰写本文时(2014年7月8日),使用CRC32来源)生成弱ETag,而使用MD5(来源)生成强ETag。

根据Express的一位贡献者所说,您可以通过以下方式指定使用强ETag或弱ETag:

app.enable('etag') // use strong etags
app.set('etag', 'strong') // same
app.set('etag', 'weak') // weak etags

看起来你也可以像这样指定自己的自定义函数来处理 ETag:

app.set('etag', function(body, encoding){ /* return valid etag */ });

另外还值得一看的是NPM包fresh,因为它在Express中用于检查更新(source1, source2)。

至于你的应用程序,请记住您可以覆盖任何响应头,例如res.set('etag', 'my-awesome-etag-value')在调用res.send()(或类似函数)之前。更多讨论(包括优缺点)请参见此处:https://github.com/visionmedia/express/issues/2129#issue-34053148


17
根据 Express 4.X 的文档,启用 etags 时默认的 etag 类型是“weak”。我想指出这一点。 - bakavic
4
更新:ETag的生成由jshttp/etag 模块完成。1.7版本(2015-06-08)始终使用MD5而不是CRC32来生成ETag,因为CRC32容易发生碰撞。下一个版本将始终使用SHA1而不是MD5,因为MD5不符合FIPS标准。最后,“weak”唯一做的就是在ETag上设置“W /”前缀。虽然express默认为“W /”,但从技术上讲,它是强ETag,因为哈希是基于正文字节而不是其语义内容计算的。如果您能够为您的应用程序实现弱ETag计算,则可以潜在地提高性能。 - ZachB
2014年的回答很棒!如果想要2021年更详细的回答,请查看其他答案。 - Aidin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接