【问题标题】:REST API: Does validation on identifiers break encapsulation?REST API:对标识符的验证是否会破坏封装?
【发布时间】:2022-11-03 07:12:40
【问题描述】:

我想我会在这里发布一些关于我最近遇到的问题的想法/反馈。我开发的 API 对作为路径参数传递的标识符进行了验证: 例如/resource/resource_identifier

关于使标识符有效的原因有一些特定的业务规则,并且我的 API 具有执行这些规则并在违反时返回 400 的验证。

现在我写这篇文章的原因是我在我写过的每一个 REST (ish) API 中都在做这种事情。它现在在我心中根深蒂固,但最近有人告诉我这是“坏的”并且它破坏了封装。此外,它通过强制消费者了解标识符的格式来做到这一点。有人告诉我,我应该返回 404 并简单地接受任何东西作为标识符。

关于这一点以及封装在 REST 上下文中的实际含义,我们进行了一些非常激烈的辩论。我找到了许多定义,但它们并不具体。与任何 REST 争论一样,很难证实两者的论点。

如果 StackOverflow 允许我,我想尝试就这一点达成共识,以及为什么像 Spotify 这样的 API 在这种情况下使用 400。

【问题讨论】:

    标签: rest


    【解决方案1】:

    虽然将资源内部 ID 公开为 URI 中使用的 ID 听起来很自然,但请记住,整个 URI 本身是资源的标识符,而不仅仅是 URI 的最后一位。客户端通常也不对构成 URI 的字符感兴趣(或者至少他们不应该关心它),但只对他们从 API/服务器请求时收到的状态感兴趣。

    此外,如果您从长远来看,这应该是您希望在 REST 架构之上构建设计的原因,资源的内部标识符是否有可能改变?如果是这样,那么引入间接可能更有意义,即通过在 URI 中使用 UUID 而不是产品 ID,然后有一个进一步的表/集合来执行从 UUID 到域对象 ID 的映射。想一想公开产品某些数据的资源。在 URI 末尾使用产品 ID 听起来可能是个好主意,因为它们可以清楚地标识您的域模型中的产品。但是,如果您的公司与另一家恰好在产品上有重叠但使用与您不同的标识符的公司合并,会发生什么?不幸的是,我在现实中看到过这样的案例,几乎所有人都希望避免对他们的客户进行更改,因此最终不得不为相同的产品支持多个 URI。

    这正是迈克阿蒙森说的原因

    ...您的数据模型不是您的对象模型不是您的资源模型...(Source

    REST 充满了这样的间接机制,以允许这样的系统避免耦合。 IE。除了上述机制之外,您还拥有链接关系,以允许服务器在需要时切换 URI,而客户端仍然可以通过公开的关系名称查找 URI,或者它专注于协商的媒体类型及其表示格式,而不是强迫客户端说出他们的特定于 API 的类似 RPC 的普通 JSON 俚语。

    Jim Webber 进一步创造了术语 domain application protocol 来描述 HTTP 是一种用于交换文档的应用程序协议,我们推断的任何业务规则都只是 HTTP 执行的实际文档管理的副作用。所以我们在“REST”中所做的基本上就是来回发送文档并推断一些业务逻辑以在接收到某些文档时采取行动。

    关于封装,这不是 REST 和 HTTP 的范围。您返回的数据取决于您的业务需求和/或交换的表示格式的功能。如果某种媒体类型无法表达某种能力,那么向客户端提供此类数据可能没有多大意义。

    一般来说,出于上述原因,我建议不要将域内部 ID 用作 URI 的一部分。通常,该信息应该是交换的有效负载的一部分,以使用户/客户可以选择在其他渠道(如电子邮件、电话等)上引用该资源……当然,这取决于资源及其手头的目的。作为用户,我更愿意用我的全名而不是一些内部用户或客户 ID 等来称呼自己。

    编辑:对不起,错过了验证方面......

    如果您希望在服务器/API 端进行用户/客户端输入,则应始终在开始处理数据之前验证数据。通常,URI 由服务器提供,并且只有在请求的 URI 与您定义的规则之一匹配时才可能触发业务活动。通常,大多数框架在无法将 URI 映射到具体操作时会以400 Bad Request 响应进行响应,从而使客户端有机会纠正其错误并重新发出更新的请求。由于 URI 无论如何都不应该由客户端生成或更改,因此验证此类参数可能是不必要的开销,除非它们可能会引入安全风险。在这里它可能是一种更好的方法,然后将 URI 的映射规则加强到操作,然后让这些框架在客户端使用他们不应该使用的东西时响应 400 消息。

    【讨论】:

    • 很棒的答案。谢谢你。您所说的关于域标识符的内容确实令人大开眼界。
    【解决方案2】:

    我在我写过的每一个 REST (ish) API 中都在做这种事情。它现在在我身上根深蒂固,但最近我被告知这是“坏的”

    在 HTTP 的上下文中,它是一种“反模式”,是的。

    我被告知我应该返回 404

    当您想要像通用 Web 服务器一样响应的优势时,这是正确的模式。

    重点是:如果您希望 HTTP 应用程序中的通用组件能够对您的响应消息执行合理的操作,那么您需要为它们提供适当的元数据。

    在满足RFC 9112中定义的request-target生产规则但其他方面不令人满意的目标资源标识符的情况下;你能够选择您想要的任何响应语义(400?403?404?499?200?)。

    但是如果你选择 404,那么通用组件就会知道响应是一个错误可以重复使用对于其他请求(在适当的条件下 - 请参阅RFC 9111)。


    为什么像 Spotify 这样的 API 在这种情况下使用 400。

    请记住:工程是关于权衡的。

    缓存的好处可能不会超过更具成本效益的请求处理,或者更有效的事件分析,或者......

    也有可能只是习惯——这样做是因为他们一直都是这样做的;或者因为他们被教导为“最佳实践”,或者其他什么。我们需要考虑的工程权衡之一是是否投资分析权衡!

    一个不完美的船舶系统比一个完美的解决方案赢得更多的市场份额。

    【讨论】:

    • 感谢您如此详细地解释这一点。您所说的关于权衡的内容正是其中的很多内容,我没有考虑过您提到的网络服务器方面。
    【解决方案3】:

    当我们想要将数据和实现隐藏在接口后面时,封装是有意义的。在这里我们要暴露数据的结构,因为它是用于通信的,而不是用于存储的,服务当然需要这种通信才能运行。数据验证是一个非常基本的概念,因为它使服务可靠并且可以防止黑客攻击。这里的 id 是一个参数,检查它的结构只是参数验证,如果失败应该返回 400。因此,这不仅限于请求的正文,问题可能出现在 HTTP 消息中的任何地方,如下所示。另一个反对 404 的论点是所请求的资源不可能存在,因为我们谈论的是格式错误的 id 和格式错误的 URI。验证每个用户输入非常重要,因为格式错误的参数可用于注入,例如如果未验证,则用于 SQL 注入。

    超文本传输​​协议 (HTTP) 400 错误请求响应状态 code 表示服务器不能或不会处理请求 由于某些被认为是客户端错误的事情(例如, 格式错误的请求语法、无效的请求消息帧,或 欺骗性请求路由)。

    对比

    HTTP 404 Not Found 响应状态码表示服务器 找不到请求的资源。导致 404 页面的链接是 通常称为断开或死链接,并且可能会受到链接腐烂。 404 状态码仅表示资源丢失:没有 缺席是暂时的还是永久的。如果资源是 永久删除,请改用 410 (Gone) 状态。

    在 REST 的情况下,我们使用 HTTP 协议、URI 标准、MIME 类型等来描述接口,而不是实际的编程语言,因为它们是独立于语言的标准。对于您的具体情况,最好检查 uniform interface constraints 包括 HATEOAS 约束,因为如果您的服务按照应有的方式生成 URI,那么很明显,格式错误的 id 是恶意的。对于 Spotify 和其他 API,其中 99% 不是 REST API,可能是 REST-ish。阅读菲尔丁论文和标准,而不是试图根据 SO 答案和示例来弄清楚。所以这是一个典型的 RTFM 情况。

    在 REST 的上下文中,一个非常简单的数据隐藏示例是存储一个数字,例如:

    PUT /x {"value": "111"} "content-type:application/vnd.example.binary+json"
    GET /x "accept:application/vnd.example.decimal+json" -> {"value": 7}
    

    在这里,我们不公开我们如何存储数据。我们只是发送它的二进制和十进制表示。这称为数据隐藏。在 id 的情况下,拥有外部 id 并将其转换为内部 id 是没有意义的,这就是为什么您在数据库中使用相同的,但可以检查其结构是否有效。通常您验证它并将其转换为 DTO。

    在这种情况下,实现隐藏更加复杂,它有点避免对服务进行微管理,而是在频繁发生时实现新功能。它可能涉及消费者调查,了解他们需要哪些功能,检查日志并找出某些消费者发送过多消息的原因以及如何将它们合并为一条消息。例如,我们有一个数学服务:

    PUT /x 7
    PUT /y 8
    PUT /z 9
    PUT /s 0
    PATCH /s {"add": "x"}
    PATCH /s {"add": "y"}
    PATCH /s {"add": "z"}
    GET /s -> 24
    
    vs
    
    POST /expression {"sum": [7,8,9]} -> 24
    

    如果你想在结构化编程、OOP 和 REST 之间进行转换,那么它是这样的:

    Number countCartTotal(CartId cartId);
    
    <=>
    
    interface iCart {
        Number countTotal();
    }
    
    <=>
    
    GET api/cart/{cartid}/total -> {total}
    

    所以一个端点代表一个暴露的操作,比如verbNoun(details),例如countCartTotal(cartId),您可以将其拆分为 verb=countTotalnoun=cartdetails=cartId 并从中构建 URI。动词必须转换为 HTTP 方法。在这种情况下,使用 GET 是最有意义的,因为我们需要数据而不是发送数据。动词的其余部分必须转换为名词,所以countTotal -&gt; GET totalCount。然后你可以合并两个名词:totalCount + cart -&gt; cartTotal。然后,您可以根据生成的名词和详细信息构建一个 URI 模板:cartTotal + cartId -&gt; cart/{cartid}/total,您就完成了端点设计GET {root}/cart/{cartid}/total。现在您可以将其绑定到countCartTotal(cartId)repo.resource(iCart, cartId).countTotal()

    所以我认为如果 id 的结构没有改变,那么如果你愿意,你甚至可以将它添加到 API 文档中。虽然没有必要这样做。

    从安全角度来看,如果发送此类请求的唯一可能原因是黑客攻击,则您可以返回 404,因此黑客无法确定失败的原因,并且您不会暴露保护的详细信息。在这种情况下,它会过度思考问题,但在某些情况下它是有意义的,例如API 可能泄漏数据的地方。例如,当您发送密码重置链接时,Web 应用程序通常会要求您提供电子邮件地址,如果未注册,大多数应用程序都会发送错误消息。这可用于检查是否有人在网站上注册,因此最好隐藏此类错误。我猜在你的情况下,id 不是敏感的东西,如果你有适当的访问控制,那么即使黑客知道 id,他们也无法对这些信息做太多事情。

    另一个可能的方面是,如果 id 的结构发生变化会怎样。好吧,我们编写了一个不同的验证代码,它只允许新结构或可能同时使用这两种结构,并使用 v2/apiv2/docs 根和文档 URI 制作新版本的 API。

    所以我完全支持你的观点,我认为你提到的其他开发人员甚至不了解 OOP 和封装,更不用说 Web 服务和 REST API。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-14
      • 1970-01-01
      • 2012-11-28
      • 2011-05-10
      • 2015-12-09
      • 1970-01-01
      相关资源
      最近更新 更多