【问题标题】:REST API design of a resource whose properties are not editable by the client客户端不可编辑属性的资源的 REST API 设计
【发布时间】:2013-11-06 19:38:40
【问题描述】:

处理必须通过不向 API 使用者公开的另一种方法修改/更新的资源属性的最佳方法是什么?

例子:

  1. 请求用于 X 的新令牌。令牌必须按照一组特定的业务规则/逻辑生成。

  2. 在旧汇率到期后请求/刷新货币汇率。该汇率仅供参考,将用于后续交易。

请注意,在上述两个示例中,值是资源的属性,而不是单独的资源。

处理这些类型的场景以及 API 使用者无法控制属性值但需要请求新属性值的其他场景的最佳方法是什么。一种选择是允许 PATCH 在请求正文中具有该特定属性,但实际上并不将属性更新为指定的值,而是运行必要的逻辑来更新属性并返回更新后的资源。

让我们更详细地看一下#1:

请求:

GET /User/1

回应:

{
   "Id": 1,
   "Email": "myemail@gmail.com",
   "SpecialToken": "12345689"
}

作为 API 的使用者,我希望能够请求一个新的SpecialToken,但生成令牌的业务规则对我来说是不可见的。

在 REST 范例中,我如何告诉 API 我需要一个新的/刷新的 SpecialToken?

一个想法是:

请求:

PATCH /User/1
{
   "SpecialToken": null
}

服务器会看到这个请求并知道它需要刷新令牌。后端会用特定算法更新SpecialToken,并返回更新后的资源:

回应:

{
   "Id": 1,
   "Email": "myemail@gmail.com",
   "SpecialToken": "99999999"
}

此示例可以扩展到示例 #2,其中SpecialToken 是资源CurrencyTrade 的汇率。 ExchangeRate 是只读值,API 的使用者不能直接更改,但可以请求更改/刷新:

请求:

GET /CurrencyTrade/1

回应:

{
   "Id": 1,
   "PropertyOne": "Value1",
   "PropertyTwo": "Value2",
   "ExchangeRate":  1.2
}

使用 API 的人需要一种方法来请求新的 ExchangeRate,但他们无法控制值是什么,严格来说是 read only property。

【问题讨论】:

  • 为什么不能直接禁止 PUT?
  • @JonathanW - 我不关心更新的授权,而是如何创建对 REST 有意义的端点以允许消费者请求更新的属性?
  • 您能否将请求消息与响应消息分开并标记?我很难跟上。
  • 我修复了一些错误并突出显示了请求/响应消息。

标签: api http rest


【解决方案1】:

您实际上是在处理资源的两种不同表示形式:一种表示客户端可以通过 POST / PUT 发送的内容,另一种表示服务器可以返回的内容。您没有处理资源本身。

能够更新令牌的要求是什么?令牌有什么用?可以根据用户中的其他值计算令牌吗?这可能只是一个示例,但上下文将推动您最终构建系统的方式。

除非有禁止它的要求,否则我可能会通过使用 PUT“触摸”资源表示来实现令牌生成场景。推测客户端无法更新 Id 字段,因此不会在客户端的表示中定义。

请求

PUT /User/1 HTTP/1.1
Content-Type: application/vnd.example.api.client+json

{
   "Email": "myemail@gmail.com"
}

回应

200 OK
Content-Type: application/vnd.example.api.server+json

{
   "Id": 1,
   "Email": "myemail@gmail.com",
   "SpecialToken": "99999999"
}

从客户端的角度来看,Email 是唯一可变的字段,因此它代表了客户端向服务器发送消息时资源的完整表示。由于服务器的响应包含额外的、不可变的信息,它实际上是在发送同一资源的不同表示。 (令人困惑的是,在现实世界中,您通常不会看到如此清楚地拼写出的媒体类型……它通常被包裹在诸如 application/json 之类的模糊内容中)。

对于您的汇率示例,我不明白为什么客户端必须告诉服务器汇率已过时。如果客户端比服务器更了解汇率的新鲜度,并且服务器正在提供价值,这不是一个很好的服务。 :) 但同样,在这样的场景中,我会像在 User 场景中那样“触摸”资源。

【讨论】:

  • 我看不出这不是部分更新,也不应该用 PUT 来完成。
  • 从客户的角度来看,这是一个完全的替代品。 Id 据称是不可变的(如果使用 URL 作为标识符,则冗余),并且 SpecialToken 不能直接从客户端更新(如前所述 - 它只能暗示请求了一个新值)。我也喜欢你的想法(作为单独的资源管理),但如果客户端不能真正 PUT 或 PATCH 到该资源(除了给出想要更新的提示),你基本上也是在做同样的事情事情,只是为了一个孤立的资源。
  • 当然,它在做同样的事情,只是我从来没有想过如果我们希望服务器用默认值填充空白,可以发送一个不完整的表示作为完全替换。非常聪明。明白了就很明显了,但我想几乎所有的知识都是这样的。 :)
  • 这真的让我思考,所以我在上面发了一个问题:stackoverflow.com/questions/19825870/…
  • 我不会说服务器填写“默认”值。相反,它在连续 GET 时提供计算的只读字段。
【解决方案2】:

有很多方法可以做到这一点。我想说最好的可能是拥有一个/User/1/SpecialToken 资源,它会提供一个202 Accepted 并带有一条消息,说明该资源无法完全删除,并且每当有人尝试时都会刷新。然后,您可以使用 DELETE、PUT 将其替换为空值,甚至使用 PATCH 直接到 SpecialToken 或 User 的属性。尽管其他人提到了什么,但将 SpecialToken 值保留在 User 资源中并没有错。客户端不必执行两个请求。

@AndyDennie 建议的方法,一个 POST 到 TokenRefresher 资源,也很好,但我更喜欢另一种方法,因为它感觉不像自定义行为。一旦在您的文档中明确说明此资源无法删除并且服务器只需刷新它,客户端就知道他可以通过任何标准化操作将其删除或设置为 null 以刷新它。

请记住,在真正的 RESTful API 中,用户的超媒体表示只会有一个标记为“刷新令牌”的链接,无论执行什么操作,URI 的语义都无关紧要。

【讨论】:

  • +1,尤其是关于超媒体链接的部分。这很关键。
  • 你能详细说明你的最后一段吗?
  • 奥马尔:看看我的新答案。这模拟了一个“令牌撤销”链接,Pedro 正在谈论的那种链接。
  • @Omar 没错。这就是我要说的。
【解决方案3】:

我认为您应该考虑将SpecialToken 设为资源,并允许API 的使用者通过POST 对其检索新实例。不知何故,您需要将User 资源链接到SpecialToken 资源。请记住,REST 的核心原则之一是您不应该依赖带外信息,因此如果您想忠于这一点,您需要调查使用链接的可能性。

首先,让我们看看你有什么:

请求:

GET /User/1
Accept: application/json

回复:

200 OK
Content-Type: application/json


{
   "Id": 1,
   "Email": "myemail@gmail.com",
   "SpecialToken": "12345689"
}

虽然此响应确实在对象中包含 SpecialToken 属性,但因为 Content-Type 是 application/json 对于未编程为理解此特定对象结构的客户端实际上没有任何意义。只理解 JSON 的客户端会像其他任何对象一样将其视为对象。让我们暂时忽略它。假设我们的想法是为SpecialToken 字段使用不同的资源;它可能看起来像这样:

请求:

GET /User/1/SpecialToken
Accept: application/json

回复:

200 OK
Content-Type: application/json

{
    "SpecialToken": "12345689"
}

因为我们做了一个GET,所以理想情况下,这个调用不应该修改资源。但是,POST 方法不遵循相同的语义。事实上,很可能向该资源发出POST 消息可能会返回不同的正文。所以让我们考虑以下几点:

请求:

POST /User/1/SpecialToken
Accept: application/json

回复:

200 OK
Content-Type: application/json

{
    "SpecialToken": "98654321"
}

注意POST 消息如何不包含正文。这可能看起来不合常规,但 HTTP 规范并没有禁止这样做,事实上 W3C TAG 说它是 all right:

请注意,即使不在 HTTP 消息正文中提供数据,也可以使用 POST。在这种情况下,资源是 URI 可寻址的,但 POST 方法向客户端指示交互不安全或可能有副作用。

听起来对我来说是正确的。过去,我听说有些服务器在处理POST 没有正文的消息时遇到了问题,但我个人对此没有任何问题。只要确保 Content-Length 标头设置正确,您就应该是金子。

因此,考虑到这一点,这似乎是一种完全有效的方式(根据 REST)来执行您的建议。但是,还记得之前我提到过关于 JSON 实际上没有任何应用程序级语义的内容吗?好吧,这意味着,为了让您的客户首先真正发送POST 以获取新的SpecialToken,它需要知道该资源的URL,或者至少知道如何制作这样的URL。这被认为是一种不好的做法,因为它将客户端与服务器联系在一起。让我们举例说明。

鉴于以下请求:

POST /User/1/SpecialToken
Accept: application/json

如果服务器不再识别 URL /User/1/SpecialToken,它可能会返回 404 或其他适当的错误消息,并且您的客户端现在已损坏。要修复它,您需要更改负责的代码。这意味着您的客户端和服务器不能彼此独立发展,并且您已经引入了耦合。但是,如果您的客户端 HTTP 例程允许您检查标头,则修复此问题相对容易。在这种情况下,您可以引入指向您的消息的链接。让我们回到我们的第一个资源:

请求:

GET /User/1
Accept: application/json

回复:

200 OK
Content-Type: application/json
Link: </User/1/SpecialToken>; rel=token

{
   "Id": 1,
   "Email": "myemail@gmail.com",
   "SpecialToken": "12345689"
}

现在在响应中,标题中指定了一个链接。这个小小的添加意味着您的客户不再需要知道如何访问SpecialToken 资源,它只需点击链接即可。虽然这并不能解决所有的耦合问题(例如,token 不是registered link relation,)但它确实有很长的路要走。您的服务器现在可以随意更改SpecialToken URL,您的客户端无需更改即可工作。

这是 HATEOAS 的一个小例子,是 Hypermedia As The Engine Of Application State 的缩写,本质上意味着您的应用程序会发现如何做事,而不是预先知道它们。首字母缩略词部门的某个人确实因此而被解雇。为了满足您对这个主题的兴趣,有一个really cool talk by Jon Moore 显示了一个广泛使用超媒体的 API。另一个不错的超媒体介绍是writings of Steve Klabnik。这应该可以帮助您入门。

希望这会有所帮助!

【讨论】:

  • 虽然我很高兴看到越来越多的人谈论并给出 HATEOAS 而非流行语 REST 的示例,但我认为您的解决方案没有多大意义,因为一旦您创建了新资源,您可以通过删除它的任何标准化操作(如 DELETE、PUT 或 PATCH)获得所需的行为。创建一个新资源然后 POST 到它是没有意义的,这需要自定义语义。
  • 别在意我之前的评论,我刚刚意识到你的意思并且同意你的看法。我会修改我的答案,感谢您的反馈!
  • 实际上,再三考虑,我不确定我的建议本身是否错误。也许非常规,但考虑到问题的参数,我认为这并不太奇怪。如果您DELETE 资源,为什么客户应该合理地认为对其执行GET 会返回一个新值?也许会,但对于DELETE 一个资源,获得肯定响应然后在随后的GET 上获得一个新值(大概是201 响应)似乎与直觉相反。同样,PATCH 或@987654359 @ 似乎违反直觉,因为无论如何都会有效地忽略该值。
  • 没错,我只是觉得没有必要同时拥有单独的资源和POST,因为你可以单独使用任何一个来解决它。可以执行 DELETE,返回 202 Accepted 并重置资源。
  • 我刚刚看了你的回答(抱歉,之前没看过!)你说的很有道理。这是个好主意,谢谢分享。阅读它,我不认为你的答案和我的答案是相互排斥的,这很酷。事实上,我认为您可以实施您的建议(尽管我可能会在PUT 和PATCH 上犯错)以及我的建议。无论如何,需要好的文档来解释发生了什么。我会留下我的答案,我认为它可以补充你的答案。
【解决方案4】:

我突然想到了另一个想法。您可以简单地将现有的特殊令牌发布到与此用户关联的 RevokedTokens 集合(假设在给定时间每个用户只允许一个特殊令牌),而不是对 RefreshToken 资源建模。

请求:

GET /User/1
Accept: application/hal+json

回应:

200 OK
Content-Type: application/hal+json

{
   _links: {
     self: { href: "/User/1" },
     "token-revocation": { href: "/User/1/RevokedTokens" }
   },
   "Id": 1,
   "Email": "myemail@gmail.com",
   "SpecialToken": "12345689"
}

遵循令牌-撤销关系并发布现有的特殊令牌将如下所示:

请求:

POST /User/1/RevokedTokens
Content-Type: text/plain

123456789

回应: 202 接受(或 204 无内容)

用户的后续 GET 将分配新的特殊令牌:

请求:

GET /User/1
Accept: application/hal+json

回应:

200 OK
Content-Type: application/hal+json

{
   _links: {
     self: { href: "/User/1" },
     "token-revocation": { href: "/User/1/RevokedTokens" }
   },
   "Id": 1,
   "Email": "myemail@gmail.com",
   "SpecialToken": "99999999"
}

这样做的优点是对可以影响其他资源的实际资源(令牌撤销列表)进行建模,而不是将服务建模为资源(即令牌刷新资源)。

【讨论】:

    【解决方案5】:

    一个单独的资源负责刷新用户资源中的令牌怎么样?

    POST /UserTokenRefresher
    {
        "User":"/User/1"
    }
    

    这可能会在响应中返回刷新后的用户表示(使用新令牌)。

    【讨论】:

    • 我可能会选择DELETE /User/1/SpecialToken,但我认为您将令牌视为另一种资源是正确的。
    • 好吧,我实际上并不是建议将令牌作为单独的资源。如果您这样做,则客户端必须发出两个请求才能获取所有用户数据。我建议您建立另一个资源,客户端可以在需要时与之交互,以请求刷新 User 资源中的令牌。
    • 另一个想法,不确定我是否更喜欢它......当你想要刷新令牌时,你可以通过另一个 URL 访问用户:GET /refreshed_user/1
    • Andy:这是个坏主意,因为这违背了 GET 的安全性和幂等性。
    猜你喜欢
    • 2017-08-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-11
    • 1970-01-01
    • 1970-01-01
    • 2022-11-02
    • 2014-04-09
    相关资源
    最近更新 更多