英文:
What this the best way to ignore unwanted fields in a JSON payload from a PUT/PATCH using Golang?
问题
我有一个情况,使用我们的 API 的人需要对我的资源进行部分更新。我知道 HTTP 清楚地指定了这是一个 PATCH 操作,尽管我们这边的人习惯于发送 PUT 请求,并且这也是遗留代码的构建方式。
举个例子,假设有以下简单的结构体:
type Person struct {
Name string
Age int
Address string
}
在 POST 请求中,我会提供一个包含所有三个值(Name、Age、Address)的有效载荷,并在我的 Golang 后端进行相应的验证。很简单。
但是在 PUT/PATCH 请求中,我们知道,例如,name
字段永远不会改变。但是假设我想要更改 age
,那么我只需发送一个包含新 age
的 JSON 有效载荷:
PUT /person/1 {age:30}
现在是我真正的问题:
如何最好地防止意外或故意修改 name
字段,以防 API 的使用者发送包含 name
字段的 JSON 有效载荷?
例如:
PUT /person/1 {name:"New Name", age:35}
我想到的可能解决方案,但实际上我并不喜欢它们,包括:
- 在我的
validator
方法中,要么强制删除不需要的name
字段,要么返回错误消息,说明name
不允许修改。 - 创建一个 DTO 对象/结构体,它几乎是我的
Person
结构体的扩展,然后将 JSON 有效载荷解组成该结构体,例如:
type PersonPut struct {
Age int
Address string
}
在我看来,这样做会增加不必要的额外代码和逻辑来抽象化这个问题,但我没有看到其他更优雅的解决方案。
我真的不喜欢这两种方法,我想知道你们是否遇到了同样的问题,以及你们是如何解决的。
谢谢!
英文:
I have a situation where people consuming our API will need to do a partial update in my resource. I understand that the HTTP clearly specifies that this is a PATCH operation, even though people on our side are used to send a PUT request for this and that's how the legacy code is built.
For exemplification, imagine the simple following struct:
type Person struct {
Name string
Age int
Address string
}
On a POST request, I will provide a payload with all three values (Name, Age, Address) and validate them accordingly on my Golang backend. Simple.
On a PUT/PATCH request though, we know that, for instance, a name
never changes. But say I would like to change the age
, then I would simply send a JSON payload containing the new age
:
PUT /person/1 {age:30}
Now to my real question:
What is the best practice to prevent name
from being used/updated intentionally or unintentionally modified in case a consumer of our API send a JSON payload containing the name
field?
Example:
PUT /person/1 {name:"New Name", age:35}
Possible solutions I thought of, but I don't actually like them, are:
-
On my
validator
method, I would either forcibly remove the unwanted fieldname
OR respond with an error message saying thatname
is not allowed. -
Create a DTO object/struct that would be pretty much an extension of my
Person
struct and then unmarshall my JSON payload into it, for instancetype PersonPut struct {
Age int
Address string
}
In my opinion this would add needless extra code and logic to abstract the problem, however I don't see any other elegant solution.
I honestly don't like those two approaches and I would like to know if you guys faced the same problem and how you solved it.
Thanks!
答案1
得分: 1
如果名称无法书写,则不应将其提供给任何更新请求。如果名称存在,我将拒绝该请求。如果我想要更宽容一些,我可能只会在名称与当前名称不同时拒绝该请求。
我不会默默地忽略与当前名称不同的名称。
英文:
If the name cannot be written it is not valid to provide it for any update request. I would reject the request if the name was present. If I wanted to be more lenient, I might consider only rejecting the request if name is different from the current name.
I would not silently ignore a name which was different from the current name.
答案2
得分: 1
你提出的第一个解决方案是一个很好的解决方案。一些知名的框架用于实现类似的逻辑。
例如,最新的Rails版本提供了一种内置解决方案,用于防止用户在请求中添加额外的数据,导致服务器在数据库中更新错误的字段。这是由ActionController::Parameters
类实现的一种白名单机制。
假设我们有一个如下所示的控制器类。为了说明问题,它包含两个update
操作。但在实际代码中你不会看到这样的写法。
class PeopleController < ActionController::Base
# 第一版本 - 不安全,会引发异常。不要这样做
def update
person = current_account.people.find(params[:id])
person.update!(params[:person])
redirect_to person
end
# 第二版本 - 只更新允许的参数
def update
person = current_account.people.find(params[:id])
person.update!(person_params) # 调用person_params方法
redirect_to person
end
private
def person_params
params.require(:person).permit(:name, :age)
end
end
由于第二个版本只允许允许的值,它将阻止用户更改有效负载并发送包含新密码值的JSON:
{ name: "acme", age: 25, password: 'account-hacked' }
有关更多详细信息,请参阅Rails文档:Action Controller Overview和ActionController::Parameters。
英文:
The first solution your brought is a good one. Some well known frameworks use to implement similar logic.
As an example, latests Rails versions come with a built in solution to prevent users to add extra data in the request, causing the server to update wrong fields in database. It is a kind of whitelist implemented by ActionController::Parameters
class.
Let's suppose we have a controller class as bellow. For purpose of this explanation, it contains two update
actions. But you won't see it in real code.
class PeopleController < ActionController::Base
# 1st version - Unsafe, it will rise an exception. Don't do it
def update
person = current_account.people.find(params[:id])
person.update!(params[:person])
redirect_to person
end
# 2nd version - Updates only permitted parameters
def update
person = current_account.people.find(params[:id])
person.update!(person_params) # call to person_params method
redirect_to person
end
private
def person_params
params.require(:person).permit(:name, :age)
end
end
Since the second version allows only permitted values, it'll block the user to change the payload and send a JSON containing a new password value:
{ name: "acme", age: 25, password: 'account-hacked' }
For more details, see Rails docs: Action Controller Overview and ActionController::Parameters
答案3
得分: 1
这可以通过首先将JSON主体解码为map[string]json.RawMessage
来解决。json.RawMessage
类型对于延迟实际解码非常有用。然后,可以在map[string]json.RawMessage
映射上应用白名单,忽略不需要的属性,只解码我们想要保留的属性的json.RawMessage
。
使用reflect
包可以自动化将白名单JSON主体解码为结构体的过程;可以在这里找到一个示例实现。
英文:
This can be solved by decoding the JSON body into a map[string]json.RawMessage
first. The json.RawMessage
type is useful for delaying the actual decoding. Afterwards, a whitelist can be applied on the map[string]json.RawMessage
map, ignoring unwanted properties and only decoding the json.RawMessage
s of the properties we want to keep.
The process of decoding the whitelisted JSON body into a struct can be automated using the reflect
package; an example implementation can be found here.
答案4
得分: 0
我对Golang不是很精通,但我认为一个好的策略是将你的name
字段转换为只读字段。
例如,在Java/.NET/C++等严格面向对象的语言中,你可以只提供一个Getter而不提供Setter。
也许Golang有一些类似Ruby的访问器配置....
如果它是只读的,那么它不应该接收多余的值,而是应该忽略它。但是,我不确定Golang是否支持这一点。
英文:
I am not proficient on Golang but I believe a good strategy would be converting your name
field to be a read-only field.
For instance, in a strictly object-oriented language as Java/.NET/C++ you can just provide a Getter but not a Setter.
Maybe there is some accessor configuration for Golang just like Ruby has....
If it is read-only then it shouldn't bother with receiving a spare value, it should just ignore it. But again, not sure if Golang supports it.
答案5
得分: 0
我认为最简洁的方法是将这个逻辑放在PATCH处理程序中。应该有一些逻辑只更新你想要的字段。如果你将其解包成map[string]string
,然后只迭代你想要更新的字段,会更容易。此外,你可以将JSON解码为一个map,删除所有不想更新的字段,重新编码为JSON,然后再解码为你的结构体。
英文:
I think the clean way is to put this logic inside the PATCH handler. There should be some logic that would update only the fields that you want. Is easier if you unpack into a map[string]string
and only iterate over the fields that you want to update. Additionally you could decode the json into a map, delete all the fields that you don't want to be updated, re-encode in json and then decode into your struct.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论