在过去十年中,HTTP社区一直忙于使Web协议现代化,对核心规范进行了多次修订和扩展,从HTTP/2到现在的HTTP/3。不幸的是,我们定义和使用HTTP标头的方式从一开始到现在都没有太大变化,由于未指定标头(以及处理它们的多种方式)导致的互操作性问题给开发人员带来了很多痛苦,它甚至引发了安全问题。文/MarkNottingham译/孟淑贤审校/江墨秋泽原文/https://www.fastly.com/blog/i...HTTPheader怎么了?大多数Web开发人员都熟悉HTTP标头;Content-Length、Cache-Control和Cookies之类的东西。它们携带有关请求和响应的元数据,通常是消息发送者出于某种原因无法放入文本中的信息,或者消息接收者无需查看文本即可获得的信息。由于标头需要由许多不同的客户端和服务器、代理服务和CDN处理(在消息的生命周期中通常不止一次),因此它们应该易于处理、高效解析并具有定义明确的语法.HTTP将标头值(更准确地说是字段值,因为它们也可能出现在正文后面的尾部字段中)定义为几乎没有约束的“八位字节序列”(即字节),尽管建议使用ASCII字节。它还建议在ABNF中定义标头,以便如果字段的值以逗号分隔,则可以将具有相同名称的多个字段组合在同一行中。因此,每个标头字段都有自己独特的定义,需要知道这些定义才能解析值。一些领域作者使用ABNF来做到这一点;其他人使用示例。有些只是让你根据以前见过的值来猜测。例如,考虑年龄标题。它是核心HTTP规范的一部分,因此应该定义明确,而且它只是一个简单的整数。Age:42由这个ABNF指定:Age=delta-secondsdelta-seconds=1*DIGITDIGIT=%x30-39;0-9起初这看起来很简单-0到9之间的数字的一对多实例。但在实际考虑中,如果实现遇到这些现实标头中的任何一个,它应该做什么:Age:0,60Age:60,0Age:50mAge:abc234Age:60;ms=212没那么简单,因为测试一个真正的缓存需要显示age。因此,当同一个人正在编写生成和使用消息头的代码而没有其他人时,example或ABNF可能是一个足够的定义,但如果有多个实现生成和解析值,则互操作性很差。每个标头编写者都必须记住解决有关如何处理重复值、大小写规范化、是单个项目还是列表等问题的列表。通常,他们不会处理这些问题,这意味着开发人员通常会选择不同的方式来处理。未指定的标头也是安全问题的来源;如果实现以不同的方式解析标头,它们的行为可能会有所不同,从而导致诸如响应拆分之类的攻击。浏览器供应商对这些问题给予了足够的重视,开始定义像CSP算法这样的标头。也就是说,他们煞费苦心地定义解析和序列化算法,然后创建测试用例。这种方法减少了字段语法的歧义,减少了实现之间的差异。但是,它仍然是一次性的;它仅有助于阐明特定标题的算法。对于规范作者来说,付出额外的努力并确保它是正确的也很累——所以大多数标题作者都不会打扰。它还为实施者带来了很多繁琐的工作,因为他们需要分别实施每个新标头的解析器。引入结构化字段HTTP工作组非常清楚这类问题,几年前我们开始尝试定义一些更好的方法来制作人们可以用来创建新字段的东西。几次尝试后,我们确定了一种最初称为结构化标头的方法,但我们现在(更准确地说)将其称为“结构化字段”。结构化字段是一个定义明确的数据类型库,可用于HTTP标头和尾部,包括字符串、令牌、布尔值、整数、小数和作为原子“项目”类型的字节序列,以及此类项目和字典的列表.重要的是,它为每种类型定义了精确的解析和序列化算法,以及错误处理和详细的测试套件——所有这些都有助于确保互操作性。这允许新标题字段的作者根据这些类型来定义它。例如,他们可以说“这是一个字符串列表”,人们就会知道如何使用现成的库来显式解析和生成标头,而不是编写特定于标头的代码。Example-Header:“blue”、“sortofred”、“green”每个项目还可以有参数,或用于附加信息的键/值对。参数是一种重要的可扩展性机制,它允许消息标头随时间演变。示例标题:“蓝色”;网络安全,“有点红”;author="sue","green"递归形式也有限制;list和dictionaryvalues也可以包含列表,eg:Example-Header:people=(joannastacy),places=("newyork""rome")innerlist中的每一项以及innerlist本身都可以参数化。您可能会注意到这些标头看起来很像许多现有的HTTP字段。这是通过设计实现的;它不仅让开发人员感到舒适,而且还允许通过结构化字段实现生成许多现有字段,而且通常还可以解析它们。例如,许多Cache-Control标头是有效的“结构化字段”,即使它没有被定义为一个:Cache-Control:max-age=3600,immutable不幸的是,您不能为现有的标头使用结构化字段,并且无法仅通过查看来判断给定字段是否为结构化字段;您必须知道它的定义值,因为结构化字段至少现在用于新字段。使用结构化字段以获得更好的性能指定新字段更容易,并使它们更安全、更具互操作性,这是对HTTP的重大改进。如果结构化字段也可以帮助HTTP性能呢?他们有两种方法可以帮助您。显然,这些都是投机性的好处,但谈论起来仍然很有趣。首先是分析效率。由于传统的HTTP标头是文本形式,解析器必须接触字符串中的每个字节,有时多次,有时将其复制并重新复制到内存的不同部分。这是一个本质上效率低下的过程,也是HTTP/2和HTTP/3是二进制而不是文本协议的原因之一。在结构化字段出现之前,我们对此无能为力,因为HTTP消息头的定义非常松散。结构化字段中定义明确的数据类型改变了这一点。现在,我们可以为任何使用它们的标头定义一个新的二进制序列化程序。二进制结构字段是定义此类序列化的提案草案。它使用HTTP/2(和/3)SETTINGS机制来协商对替代序列化的支持,并利用结构化字段与许多现有标头字段的句法相似性将它们返回到已经广泛使用的标头字段集,回退到不透??明如果无法解析文本。二进制序列化对性能有多大帮助?由于CPU负载的预期减少,它应该减少请求处理的延迟并提高可伸缩性。我们还没有真正的统计数据,但如果你想一想许多游戏所采用的路径——从JavaScript到浏览器,然后到CDN,通过多个CDN节点到源服务器,然后到应用程序代码本身。累积节省的潜力很有吸引力。结构化字段有助于提高性能的第二种方式是提高压缩效率。HTTP/2为标头和尾部字段引入了HPACK压缩。虽然其前身SPDY使用GZIP,但由于CRIME攻击而被发现不安全。因此,HPACK(及其后继QPACK)通过引用整个字段值来压缩字段;如果它的任何部分发生变化,它就不能使用以前的参考(有时会对压缩效率产生惊人的影响)。选择整数粒度是因为通用解析器无法理解字段值的结构;为了安全起见,我们必须确保攻击者无法通过猜测部分字段值来探测加密。对于结构化字段,压缩算法现在有一种潜在的方法可以对字段中的单个数据类型而不是整个值进行操作。Cache-Control:max-age=3600,s-maxage=7200,must-revalidate例如,考虑以下Cache-Control字段:对于HPACK和QPACK,整个字段值存储在动态表中,并且可以仅由具有完全相同值的Future消息引用使用。如果我们将其解析为结构化字段并存储单一数据类型,我们可以存储:lmax-agel3600ls-maxagel7200lmust-revalidate这些变量中的每一个都可以在未来的标头中单独引用,从而使压缩算法更精细和更高效。早期的原型表明,使用这种技术进行提升对于Web浏览器连接来说效率非常低,因为它们的标头往往是高度重复的,用多个字节(字段值部分中的每种类型一个单词)替换HPACK中的1字节引用实际上会造成伤害.对于承载来自多个客户端的流量的连接——例如反向代理和原始服务器上游的CDN所看到的连接——好处可能更加明显;需要更多的实验。HTTP的长期改进如果发现上述反向导入技术,HTTP的未来版本(或HTTP/2和HTTP/3的扩展)可能会大大减少使用的非结构化标头的数量。BinaryStructuredFields草案描述了两种实现方式。如果该字段的语法与结构化字段兼容-至少在大多数情况下-它可以作为一个发送,失败时回退到明文标题。没有兼容语法的标头需要另一种方法。例如,Date、Last-Modified、Expires和类似的标头永远不可能是有效的结构化字段。但是,日期可以表示为整数,结构化字段可以传递整数。因此,像这样的标头:Date:Thu,09Apr202009:06:50GMT可能会在适当的翻译跳转中表示为:SF-Date:1586423210这为我们提供了一种将所有通用消息标头和附加元信息转换为作为结构化字段发送。立即使用结构化字段结构化字段规范正处于标准化的最后阶段,这意味着它将很快成为RFC。我们已经有多个实例,包括在Chrome中,其中许多新的安全标头(例如Fetch元数据)都是结构化的。同时,可以通过实施它们来了解它们的工作原理。例如,Pythonhttp_sfv库允许从命令行解析它们。如果您定义了新的标头(无论它们是用于整个网络还是仅用于HTTPAPI),您可以在RFC发布后开始使用结构化字段。
