本文介绍如何处理 ASP.NET Core Web API 中的 JSON 修补程序请求。
ASP.NET Core Web API 中的 JSON Patch 支持基于 System.Text.Json 序列化,并需要 Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet 包。
JSON Patch 标准是什么?
JSON 补丁标准
描述要应用于 JSON 文档的更改的标准格式。
在 RFC 6902 中定义,在 RESTful API 中广泛使用,以对 JSON 资源执行部分更新。
描述修改 JSON 文档的作序列,例如:
add
remove
replace
move
copy
test
在web应用中,JSON Patch通常用于PATCH操作以执行资源的部分更新。 与其发送整个资源进行更新,客户端可以发送仅包含更改的 JSON Patch 文档。 修补可减少有效负载大小并提高效率。
有关 JSON 修补程序标准的概述,请参阅 jsonpatch.com。
ASP.NET Core Web API 中的 JSON 修补程序支持
ASP.NET Core Web API 中的 JSON 修补程序支持基于 System.Text.Json 序列化,从 .NET 10 开始,并实现基于 System.Text.Json 序列化的 Microsoft.AspNetCore.JsonPatch。 此功能:
- 需要
Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet 包。 - 通过利用针对 .NET 进行优化的 System.Text.Json 库,与现代 .NET 实践相一致。
- 与基于旧
Newtonsoft.Json
版的实现相比,提供改进的性能和减少的内存使用量。 有关基于旧Newtonsoft.Json
版的实现的详细信息,请参阅 本文的 .NET 9 版本。
注释
基于System.Text.Json序列化的Microsoft.AspNetCore.JsonPatch实现不能直接替代旧Newtonsoft.Json
实现。 它不支持动态类型,例如 ExpandoObject。
重要
JSON 修补程序标准具有 固有的安全风险。 由于这些风险固有于 JSON 修补程序标准,因此 ASP.NET Core 实现 不会尝试缓解固有的安全风险。 开发人员有责任确保 JSON 修补程序文档可以安全地应用于目标对象。 有关详细信息,请参阅“ 缓解安全风险 ”部分。
通过 System.Text.Json 启用 JSON 修补程序支持
要通过 System.Text.Json 启用 JSON 修补程序支持,请安装 Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet 包。
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease
此包提供一个 JsonPatchDocument<TModel> 类,用于表示类型 T
对象的 JSON Patch 文档,并使用 System.Text.Json 提供用于序列化和反序列化 JSON Patch 文档的自定义逻辑。 类JsonPatchDocument<TModel>的关键方法是ApplyTo(Object),它将修补操作应用于类型T
的目标对象。
应用 JSON Patch 的动作方法代码
在 API 控制器中,JSON 修补程序的操作方法:
- 使用 HttpPatchAttribute 属性进行批注。
- 接受 JsonPatchDocument<TModel>,通常带有 FromBodyAttribute。
- 在修补程序文档上调用 ApplyTo(Object) 以应用更改。
控制器动作方法示例:
[HttpPatch("{id}", Name = "UpdateCustomer")]
public IActionResult Update(AppDb db, string id, [FromBody] JsonPatchDocument<Customer> patchDoc)
{
// Retrieve the customer by ID
var customer = db.Customers.FirstOrDefault(c => c.Id == id);
// Return 404 Not Found if customer doesn't exist
if (customer == null)
{
return NotFound();
}
patchDoc.ApplyTo(customer, jsonPatchError =>
{
var key = jsonPatchError.AffectedObject.GetType().Name;
ModelState.AddModelError(key, jsonPatchError.ErrorMessage);
}
);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return new ObjectResult(customer);
}
示例应用中的此代码适用于以下 Customer
和 Order
模型:
namespace App.Models;
public class Customer
{
public string Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public string? Address { get; set; }
public List<Order>? Orders { get; set; }
public Customer()
{
Id = Guid.NewGuid().ToString();
}
}
namespace App.Models;
public class Order
{
public string Id { get; set; }
public DateTime? OrderDate { get; set; }
public DateTime? ShipDate { get; set; }
public decimal TotalAmount { get; set; }
public Order()
{
Id = Guid.NewGuid().ToString();
}
}
示例操作方法的关键步骤:
- 检索客户:
- 该方法使用提供的 ID 从数据库中
AppDb
检索Customer
对象。 Customer
如果未找到任何对象,则返回响应404 Not Found
。
- 该方法使用提供的 ID 从数据库中
- 应用 JSON 修补程序:
- 该ApplyTo(Object)方法对检索到的
Customer
对象应用来自patchDoc中的JSON Patch操作。 - 如果在应用修补程序期间发生错误(例如无效操作或冲突),错误处理委托会捕获错误。 此委托使用受影响对象的类型名称和错误消息将错误消息添加到
ModelState
。
- 该ApplyTo(Object)方法对检索到的
- 验证 ModelState:
- 应用修补程序后,该方法会检查
ModelState
是否存在错误。 - 如果
ModelState
无效,例如因为补丁错误,它将返回400 Bad Request
响应,其中包含验证错误。
- 应用修补程序后,该方法会检查
- 返回更新的客户信息:
- 如果修补程序成功应用且
ModelState
有效,该方法在响应中返回更新Customer
的对象。
- 如果修补程序成功应用且
错误响应示例:
当指定的路径无效时,以下示例展示了 JSON Patch 操作的响应正文 400 Bad Request
。
{
"Customer": [
"The target location specified by path segment 'foobar' was not found."
]
}
将 JSON 修补程序文档应用于对象
以下示例演示如何使用 ApplyTo(Object) 该方法将 JSON 修补程序文档应用于对象。
示例:向对象应用JsonPatchDocument<TModel>
下面的示例展示了如何:
add
、replace
和remove
操作。- 对嵌套属性的操作。
- 向数组添加新项。
- 在 JSON 修补程序文档中使用 JSON 字符串枚举转换器。
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com",
PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],
Address = new Address
{
Street = "123 Main St",
City = "Anytown",
State = "TX"
}
};
// Raw JSON patch document
string jsonPatch = """
[
{ "op": "replace", "path": "/FirstName", "value": "Jane" },
{ "op": "remove", "path": "/Email"},
{ "op": "add", "path": "/Address/ZipCode", "value": "90210" },
{ "op": "add", "path": "/PhoneNumbers/-", "value": { "Number": "987-654-3210",
"Type": "Work" } }
]
""";
// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON patch document
patchDoc!.ApplyTo(person);
// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
上一个示例生成更新对象的以下输出:
{
"firstName": "Jane",
"lastName": "Doe",
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "TX",
"zipCode": "90210"
},
"phoneNumbers": [
{
"number": "123-456-7890",
"type": "Mobile"
},
{
"number": "987-654-3210",
"type": "Work"
}
]
}
ApplyTo(Object) 方法通常遵循 System.Text.Json 的约定和选项来处理 JsonPatchDocument<TModel>,包括由以下选项控制的行为:
- JsonNumberHandling:是否从字符串中读取数值属性。
- PropertyNameCaseInsensitive:属性名称是否区分大小写。
System.Text.Json和新JsonPatchDocument<TModel>实现之间的主要区别:
- 目标对象的运行时类型,而不是所声明的类型,决定了 ApplyTo(Object) 会修补哪些属性。
- System.Text.Json 反序列化依赖于声明的类型来标识符合条件的属性。
示例:应用 JsonPatchDocument 并进行错误处理
应用 JSON 修补程序文档时可能会出现各种错误。 例如,目标对象可能没有指定的属性,或者指定的值可能与属性类型不兼容。
JSON Patch
支持 test
操作,该操作用于检查指定的值是否等于目标属性。 如果没有,则返回错误。
以下示例演示如何正常处理这些错误。
重要
传递给 ApplyTo(Object) 方法的对象被就地修改。 如果任何操作失败,调用方负责放弃更改。
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com"
};
// Raw JSON patch document
string jsonPatch = """
[
{ "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},
{ "op": "test", "path": "/FirstName", "value": "Jane" },
{ "op": "replace", "path": "/LastName", "value": "Smith" }
]
""";
// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON patch document, catching any errors
Dictionary<string, string[]>? errors = null;
patchDoc!.ApplyTo(person, jsonPatchError =>
{
errors ??= new ();
var key = jsonPatchError.AffectedObject.GetType().Name;
if (!errors.ContainsKey(key))
{
errors.Add(key, new string[] { });
}
errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
});
if (errors != null)
{
// Print the errors
foreach (var error in errors)
{
Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");
}
}
// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
前面的示例生成以下输出:
Error in Person: The current value 'John' at path 'FirstName' is not equal
to the test value 'Jane'.
{
"firstName": "John",
"lastName": "Smith", <<< Modified!
"email": "janedoe@gmail.com", <<< Modified!
"phoneNumbers": []
}
减轻安全风险
使用 Microsoft.AspNetCore.JsonPatch.SystemTextJson
包时,了解和缓解潜在的安全风险至关重要。 以下部分概述了与 JSON 修补程序关联的已识别的安全风险,并提供建议的缓解措施,以确保包的安全使用。
重要
这不是威胁的详尽列表。 应用开发人员必须进行自己的威胁模型评审,以确定特定于应用的综合列表,并根据需要提出适当的缓解措施。 例如,向修补操作公开集合的应用程序,应考虑到如果这些操作在集合开头插入或删除元素,可能会引发算法复杂性攻击。
若要在将 JSON 修补程序功能集成到其应用中时最大程度地降低安全风险,开发人员应:
- 为自己的应用运行全面的威胁模型。
- 解决已识别的威胁。
- 请遵循以下部分中的建议缓解措施进行操作。
通过内存放大拒绝服务 (DoS)
- 场景:恶意客户端提交了一个
copy
操作,该操作多次重复复制大型对象图,导致内存过度消耗。 - 影响:潜在的内存不足 (OOM) 条件,导致服务中断。
- 缓解措施:
- 在调用 ApplyTo(Object)之前验证传入的 JSON 补丁文档的大小和结构。
- 验证必须特定于应用,但示例验证可能类似于以下内容:
public void Validate(JsonPatchDocument<T> patch)
{
// This is just an example. It's up to the developer to make sure that
// this case is handled properly, based on the app needs.
if (patch.Operations.Where(op=>op.OperationType == OperationType.Copy).Count()
> MaxCopyOperationsCount)
{
throw new InvalidOperationException();
}
}
业务逻辑 Subversion
- 场景:修补操作可以处理具有隐式不变量的字段(例如内部标志、ID 或计算字段),从而违反业务约束。
- 影响:数据完整性问题和意外的应用行为。
- 缓解措施:
- 将 POCO(普通旧 CLR 对象)与明确定义的属性一起使用,这些属性可以安全地进行修改。
- 避免在目标对象中公开敏感或安全关键属性。
- 如果未使用 POCO 对象,请在应用操作后验证修补的对象,以确保不违反业务规则和不变量。
- 将 POCO(普通旧 CLR 对象)与明确定义的属性一起使用,这些属性可以安全地进行修改。
身份验证和授权
- 场景:未经身份验证或未经授权的客户端发送恶意 JSON 修补请求。
- 影响:未经授权的访问以修改敏感数据或中断应用行为。
- 缓解措施:
- 使用适当的身份验证和授权机制保护接受 JSON 修补请求的终结点。
- 限制对具有适当权限的受信任客户端或用户的访问权限。
获取代码
若要测试此示例,请使用以下设置运行应用并发送 HTTP 请求:
- URL:
http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
- HTTP 方法:
PATCH
- 标头:
Content-Type: application/json-patch+json
- 正文:从 JSON 项目文件夹中复制并粘贴其中一个 JSON 修补程序文档示例。
其他资源
本文介绍如何处理 ASP.NET Core Web API 中的 JSON 修补程序请求。
重要
JSON 修补程序标准具有 固有的安全风险。 此实现 不会尝试缓解这些固有的安全风险。 开发人员有责任确保 JSON 修补程序文档可以安全地应用于目标对象。 有关详细信息,请参阅“ 缓解安全风险 ”部分。
包安装
ASP.NET Core Web API 中的 JSON 修补程序支持基于 Newtonsoft.Json
,并且需要 Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet 包。
要启用 JSON 修补程序支持,请执行以下操作:
安装
Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet 包。调用 AddNewtonsoftJson。 例如:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers() .AddNewtonsoftJson(); var app = builder.Build(); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
AddNewtonsoftJson
替换了基于默认 System.Text.Json
的输入和输出格式化程序,该格式化程序用于设置所有 JSON 内容的格式。 此扩展方法与以下 MVC 服务注册方法兼容:
JsonPatch 要求将 Content-Type
标头设置为 application/json-patch+json
。
使用 System.Text.Json 时添加对 JSON 修补程序的支持
基于 System.Text.Json
的输入格式化程序不支持 JSON 修补程序。 要使用 Newtonsoft.Json
添加对 JSON 修补程序的支持,同时使其他输入和输出格式化程序保持不变,请执行以下操作:
安装
Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet 包。更新
Program.cs
:using JsonPatchSample; using Microsoft.AspNetCore.Mvc.Formatters; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { options.InputFormatters.Insert(0, MyJPIF.GetJsonPatchInputFormatter()); }); var app = builder.Build(); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Options; namespace JsonPatchSample; public static class MyJPIF { public static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter() { var builder = new ServiceCollection() .AddLogging() .AddMvc() .AddNewtonsoftJson() .Services.BuildServiceProvider(); return builder .GetRequiredService<IOptions<MvcOptions>>() .Value .InputFormatters .OfType<NewtonsoftJsonPatchInputFormatter>() .First(); } }
前面的代码创建 NewtonsoftJsonPatchInputFormatter 的实例,并将其作为 MvcOptions.InputFormatters 集合中的第一个条目插入。 此注册顺序可确保:
-
NewtonsoftJsonPatchInputFormatter
处理 JSON 修补程序请求。 - 基于
System.Text.Json
的现有输入和格式化程序处理所有其他 JSON 请求和响应。
使用 Newtonsoft.Json.JsonConvert.SerializeObject
方法序列化 JsonPatchDocument。
PATCH HTTP 请求方法
PUT 和 PATCH 方法用于更新现有资源。 它们之间的区别是,PUT 会替换整个资源,而PATCH 仅指定更改。
JSON Patch
JSON 修补程序是一种格式,用于指定要应用于资源的更新。 JSON 修补程序文档有一个操作数组。 每个操作都标识一种特定类型的更改。 此类更改的示例包括添加数组元素或替换属性值。
例如,以下 JSON 文档表示资源、资源的 JSON Patch 文档和应用 Patch 操作的结果。
资源示例
{
"customerName": "John",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
}
]
}
JSON 修补程序示例
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
在上述 JSON 中:
op
属性指示操作的类型。path
属性指示要更新的元素。value
属性提供新值。
修补程序之后的资源
下面是应用上述 JSON 修补程序文档后的资源:
{
"customerName": "Barry",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
},
{
"orderName": "Order2",
"orderType": null
}
]
}
通过将 JSON Patch 文档应用于资源所做的更改是原子操作。 如果列表中的任何操作失败,则不会应用列表中的任何操作。
路径语法
操作对象的路径属性的级别之间有斜杠。 例如,"/address/zipCode"
。
使用从零开始的索引来指定数组元素。 addresses
数组的第一个元素将位于 /addresses/0
。 若要将 add
置于数组末尾,请使用连字符 (-
),而不是索引号:/addresses/-
。
操作
下表显示了 JSON 修补程序规范中定义的支持操作:
操作 | 备注 |
---|---|
add |
添加属性或数组元素。 对于现有属性:设置值。 |
remove |
删除属性或数组元素。 |
replace |
与在相同位置后跟 add 的 remove 相同。 |
move |
与从后跟 add 的源到使用源中的值的目标的 remove 相同。 |
copy |
与到使用源中的值的目标的 add 相同。 |
test |
如果 path 处的值 = 提供的 value ,则返回成功状态代码。 |
ASP.NET Core 中的 JSON Patch
Microsoft.AspNetCore.JsonPatch NuGet 包中提供了 JSON 修补程序的 ASP.NET Core 实现。
操作方法代码
在 API 控制器中,JSON 修补程序的操作方法:
- 使用
HttpPatch
属性进行批注。 - 接受 JsonPatchDocument<TModel>,通常带有
[FromBody]
。 - 在修补程序文档上调用 ApplyTo(Object) 以应用更改。
下面是一个示例:
[HttpPatch]
public IActionResult JsonPatchWithModelState(
[FromBody] JsonPatchDocument<Customer> patchDoc)
{
if (patchDoc != null)
{
var customer = CreateCustomer();
patchDoc.ApplyTo(customer, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return new ObjectResult(customer);
}
else
{
return BadRequest(ModelState);
}
}
示例应用中的此代码适用于以下 Customer
模型:
namespace JsonPatchSample.Models;
public class Customer
{
public string? CustomerName { get; set; }
public List<Order>? Orders { get; set; }
}
namespace JsonPatchSample.Models;
public class Order
{
public string OrderName { get; set; }
public string OrderType { get; set; }
}
示例操作方法:
- 构造一个
Customer
。 - 应用修补程序。
- 在响应的正文中返回结果。
在实际应用中,该代码将从存储(如数据库)中检索数据并在应用修补程序后更新数据库。
模型状态
上述操作方法示例调用将模型状态用作其参数之一的 ApplyTo
的重载。 使用此选项,可以在响应中收到错误消息。 以下示例显示了 test
操作的“400 错误请求”响应的正文:
{
"Customer": [
"The current value 'John' at path 'customerName' != test value 'Nancy'."
]
}
动态对象
以下操作方法示例演示如何将修补程序应用于动态对象:
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
dynamic obj = new ExpandoObject();
patch.ApplyTo(obj);
return Ok(obj);
}
添加操作
- 如果
path
指向数组元素:将新元素插入到path
指定的元素之前。 - 如果
path
指向属性:设置属性值。 - 如果
path
指向不存在的位置:- 如果要修补的资源是一个动态对象:添加属性。
- 如果要修补的资源是一个静态对象:请求失败。
以下示例修补程序文档设置 CustomerName
的值,并将 Order
对象添加到 Orders
数组末尾。
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
删除操作
- 如果
path
指向数组元素:删除元素。 - 如果
path
指向属性:- 如果要修补的资源是一个动态对象:删除属性。
- 如果要修补的资源是一个静态对象:
- 如果属性可以为 Null:将其设置为 null。
- 如果属性不可以为 Null,将其设置为
default<T>
。
以下示例修补程序文档将 CustomerName
设置为 null 并删除 Orders[0]
:
[
{
"op": "remove",
"path": "/customerName"
},
{
"op": "remove",
"path": "/orders/0"
}
]
替换操作
此操作在功能上与后跟 add
的 remove
相同。
以下示例修补程序文档设置 CustomerName
的值,并将 Orders[0]
替换为新的 Order
对象:
[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
移动操作
- 如果
path
指向数组元素:将from
元素复制到path
元素的位置,然后对from
元素运行remove
操作。 - 如果
path
指向属性:将from
属性的值复制到path
属性,然后对from
属性运行remove
操作。 - 如果
path
指向不存在的属性:- 如果要修补的资源是一个静态对象:请求失败。
- 如果要修补的资源是一个动态对象:将
from
属性复制到path
指示的位置,然后对from
属性运行remove
操作。
以下示例修补程序文档:
- 将
Orders[0].OrderName
的值复制到CustomerName
。 - 将
Orders[0].OrderName
设置为 null。 - 将
Orders[1]
移动到Orders[0]
之前。
[
{
"op": "move",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "move",
"from": "/orders/1",
"path": "/orders/0"
}
]
复制操作
此操作在功能上与不包含最后 remove
步骤的 move
操作相同。
以下示例修补程序文档:
- 将
Orders[0].OrderName
的值复制到CustomerName
。 - 将
Orders[1]
的副本插入到Orders[0]
之前。
[
{
"op": "copy",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "copy",
"from": "/orders/1",
"path": "/orders/0"
}
]
测试操作
如果 path
指示的位置处的值与 value
中提供的值不同,则请求会失败。 在这种情况下,整个 PATCH 请求会失败,即使修补程序文档中的所有其他操作均成功也是如此。
test
操作通常用于在发生并发冲突时阻止更新。
如果 CustomerName
的初始值是“John”,则以下示例修补程序文档不起任何作用,因为测试失败:
[
{
"op": "test",
"path": "/customerName",
"value": "Nancy"
},
{
"op": "add",
"path": "/customerName",
"value": "Barry"
}
]
获取代码
若要测试此示例,请使用以下设置运行应用并发送 HTTP 请求:
- URL:
http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
- HTTP 方法:
PATCH
- 标头:
Content-Type: application/json-patch+json
- 正文:从 JSON 项目文件夹中复制并粘贴其中一个 JSON 修补程序文档示例。
减轻安全风险
在将 Microsoft.AspNetCore.JsonPatch
包与基于 Newtonsoft.Json
的实现一起使用时,了解和缓解潜在的安全风险至关重要。 以下部分概述了与 JSON 修补程序关联的已识别的安全风险,并提供建议的缓解措施,以确保包的安全使用。
重要
这不是威胁的详尽列表。 应用开发人员必须进行自己的威胁模型评审,以确定特定于应用的综合列表,并根据需要提出适当的缓解措施。 例如,向修补操作公开集合的应用程序,应考虑到如果这些操作在集合开头插入或删除元素,可能会引发算法复杂性攻击。
通过运行自己的应用的综合威胁模型并解决已识别的威胁,同时遵循以下建议的缓解措施,这些包的使用者可以将 JSON 修补程序功能集成到其应用中,同时最大程度地降低安全风险。
通过内存放大拒绝服务 (DoS)
- 场景:恶意客户端提交了一个
copy
操作,该操作多次重复复制大型对象图,导致内存过度消耗。 - 影响:潜在的内存不足 (OOM) 条件,导致服务中断。
- 缓解措施:
- 在调用
ApplyTo
之前验证传入的 JSON 补丁文档的大小和结构。 - 验证需要特定于应用,但示例验证可能类似于以下内容:
- 在调用
public void Validate(JsonPatchDocument patch)
{
// This is just an example. It's up to the developer to make sure that
// this case is handled properly, based on the app needs.
if (patch.Operations.Where(op => op.OperationType == OperationType.Copy).Count()
> MaxCopyOperationsCount)
{
throw new InvalidOperationException();
}
}
业务逻辑 Subversion
- 场景:修补操作可以处理隐式不变量字段(例如内部标志、ID 或计算得到的字段),从而违反业务约束。
- 影响:数据完整性问题和意外的应用行为。
- 缓解措施:
- 将 POCO 对象与显式定义的属性一起使用,这些属性可以安全地进行修改。
- 避免在目标对象中公开敏感或安全关键属性。
- 如果未使用 POCO 对象,请在应用操作后验证已修补对象,以确保不违反业务规则和不可变条件。
身份验证和授权
- 场景:未经身份验证或未经授权的客户端发送恶意 JSON 修补请求。
- 影响:未经授权的访问以修改敏感数据或中断应用行为。
- 缓解措施:
- 使用适当的身份验证和授权机制保护接受 JSON 修补请求的终结点。
- 限制对具有适当权限的受信任客户端或用户的访问权限。
其他资源
本文介绍如何处理 ASP.NET Core Web API 中的 JSON 修补程序请求。
重要
JSON 修补程序标准具有 固有的安全风险。 由于这些风险固有于 JSON 修补程序标准,因此此实现 不会尝试降低固有的安全风险。 开发人员有责任确保 JSON 修补程序文档可以安全地应用于目标对象。 有关详细信息,请参阅“ 缓解安全风险 ”部分。
包安装
要在应用中启用 JSON Patch 支持,请完成以下步骤:
安装
Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet 包。更新项目的
Startup.ConfigureServices
方法以调用 AddNewtonsoftJson。 例如:services .AddControllersWithViews() .AddNewtonsoftJson();
AddNewtonsoftJson
与 MVC 服务注册方法兼容:
JSON Patch、AddNewtonsoftJson 和 System.Text.Json
AddNewtonsoftJson
替换了用于格式化所有 JSON 内容的基于 System.Text.Json
的输入和输出格式化程序。 要使用 Newtonsoft.Json
添加对 JSON Patch 的支持,同时使其他格式化程序保持不变,请按如下所示更新项目的 Startup.ConfigureServices
方法:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options =>
{
options.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
});
}
private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
{
var builder = new ServiceCollection()
.AddLogging()
.AddMvc()
.AddNewtonsoftJson()
.Services.BuildServiceProvider();
return builder
.GetRequiredService<IOptions<MvcOptions>>()
.Value
.InputFormatters
.OfType<NewtonsoftJsonPatchInputFormatter>()
.First();
}
上述代码需要 Microsoft.AspNetCore.Mvc.NewtonsoftJson
包和以下 using
语句:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Linq;
使用 Newtonsoft.Json.JsonConvert.SerializeObject
方法可序列化 JsonPatchDocument。
PATCH HTTP 请求方法
PUT 和 PATCH 方法用于更新现有资源。 它们之间的区别是,PUT 会替换整个资源,而PATCH 仅指定更改。
JSON Patch
JSON 修补程序是一种格式,用于指定要应用于资源的更新。 JSON 修补程序文档有一个操作数组。 每个操作都标识一种特定类型的更改。 此类更改的示例包括添加数组元素或替换属性值。
例如,以下 JSON 文档表示资源、资源的 JSON Patch 文档和应用 Patch 操作的结果。
资源示例
{
"customerName": "John",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
}
]
}
JSON 修补程序示例
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
在上述 JSON 中:
op
属性指示操作的类型。path
属性指示要更新的元素。value
属性提供新值。
修补程序之后的资源
下面是应用上述 JSON 修补程序文档后的资源:
{
"customerName": "Barry",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
},
{
"orderName": "Order2",
"orderType": null
}
]
}
通过将 JSON Patch 文档应用于资源所做的更改是原子操作。 如果列表中的任何操作失败,则不会应用列表中的任何操作。
路径语法
操作对象的路径属性的级别之间有斜杠。 例如,"/address/zipCode"
。
使用从零开始的索引来指定数组元素。 addresses
数组的第一个元素将位于 /addresses/0
。 若要将 add
置于数组末尾,请使用连字符 (-
),而不是索引号:/addresses/-
。
操作
下表显示了 JSON 修补程序规范中定义的支持操作:
操作 | 备注 |
---|---|
add |
添加属性或数组元素。 对于现有属性:设置值。 |
remove |
删除属性或数组元素。 |
replace |
与在相同位置后跟 add 的 remove 相同。 |
move |
与从后跟 add 的源到使用源中的值的目标的 remove 相同。 |
copy |
与到使用源中的值的目标的 add 相同。 |
test |
如果 path 处的值 = 提供的 value ,则返回成功状态代码。 |
ASP.NET Core 中的 JSON Patch
Microsoft.AspNetCore.JsonPatch NuGet 包中提供了 JSON 修补程序的 ASP.NET Core 实现。
操作方法代码
在 API 控制器中,JSON 修补程序的操作方法:
- 使用
HttpPatch
属性进行批注。 - 接受
JsonPatchDocument<T>
,通常带有[FromBody]
。 - 在修补程序文档上调用
ApplyTo
以应用更改。
下面是一个示例:
[HttpPatch]
public IActionResult JsonPatchWithModelState(
[FromBody] JsonPatchDocument<Customer> patchDoc)
{
if (patchDoc != null)
{
var customer = CreateCustomer();
patchDoc.ApplyTo(customer, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return new ObjectResult(customer);
}
else
{
return BadRequest(ModelState);
}
}
示例应用中的此代码适用于以下 Customer
模型:
using System.Collections.Generic;
namespace JsonPatchSample.Models
{
public class Customer
{
public string CustomerName { get; set; }
public List<Order> Orders { get; set; }
}
}
namespace JsonPatchSample.Models
{
public class Order
{
public string OrderName { get; set; }
public string OrderType { get; set; }
}
}
示例操作方法:
- 构造一个
Customer
。 - 应用修补程序。
- 在响应的正文中返回结果。
在实际应用中,该代码将从存储(如数据库)中检索数据并在应用修补程序后更新数据库。
模型状态
上述操作方法示例调用将模型状态用作其参数之一的 ApplyTo
的重载。 使用此选项,可以在响应中收到错误消息。 以下示例显示了 test
操作的“400 错误请求”响应的正文:
{
"Customer": [
"The current value 'John' at path 'customerName' is not equal to the test value 'Nancy'."
]
}
动态对象
以下操作方法示例演示如何将修补程序应用于动态对象:
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
dynamic obj = new ExpandoObject();
patch.ApplyTo(obj);
return Ok(obj);
}
添加操作
- 如果
path
指向数组元素:将新元素插入到path
指定的元素之前。 - 如果
path
指向属性:设置属性值。 - 如果
path
指向不存在的位置:- 如果要修补的资源是一个动态对象:添加属性。
- 如果要修补的资源是一个静态对象:请求失败。
以下示例修补程序文档设置 CustomerName
的值,并将 Order
对象添加到 Orders
数组末尾。
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
删除操作
- 如果
path
指向数组元素:删除元素。 - 如果
path
指向属性:- 如果要修补的资源是一个动态对象:删除属性。
- 如果要修补的资源是一个静态对象:
- 如果属性可以为 Null:将其设置为 null。
- 如果属性不可以为 Null,将其设置为
default<T>
。
以下示例修补程序文档将 CustomerName
设置为 null 并删除 Orders[0]
:
[
{
"op": "remove",
"path": "/customerName"
},
{
"op": "remove",
"path": "/orders/0"
}
]
替换操作
此操作在功能上与后跟 add
的 remove
相同。
以下示例修补程序文档设置 CustomerName
的值,并将 Orders[0]
替换为新的 Order
对象:
[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
移动操作
- 如果
path
指向数组元素:将from
元素复制到path
元素的位置,然后对from
元素运行remove
操作。 - 如果
path
指向属性:将from
属性的值复制到path
属性,然后对from
属性运行remove
操作。 - 如果
path
指向不存在的属性:- 如果要修补的资源是一个静态对象:请求失败。
- 如果要修补的资源是一个动态对象:将
from
属性复制到path
指示的位置,然后对from
属性运行remove
操作。
以下示例修补程序文档:
- 将
Orders[0].OrderName
的值复制到CustomerName
。 - 将
Orders[0].OrderName
设置为 null。 - 将
Orders[1]
移动到Orders[0]
之前。
[
{
"op": "move",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "move",
"from": "/orders/1",
"path": "/orders/0"
}
]
复制操作
此操作在功能上与不包含最后 remove
步骤的 move
操作相同。
以下示例修补程序文档:
- 将
Orders[0].OrderName
的值复制到CustomerName
。 - 将
Orders[1]
的副本插入到Orders[0]
之前。
[
{
"op": "copy",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "copy",
"from": "/orders/1",
"path": "/orders/0"
}
]
测试操作
如果 path
指示的位置处的值与 value
中提供的值不同,则请求会失败。 在这种情况下,整个 PATCH 请求会失败,即使修补程序文档中的所有其他操作均成功也是如此。
test
操作通常用于在发生并发冲突时阻止更新。
如果 CustomerName
的初始值是“John”,则以下示例修补程序文档不起任何作用,因为测试失败:
[
{
"op": "test",
"path": "/customerName",
"value": "Nancy"
},
{
"op": "add",
"path": "/customerName",
"value": "Barry"
}
]
获取代码
若要测试此示例,请使用以下设置运行应用并发送 HTTP 请求:
- URL:
http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
- HTTP 方法:
PATCH
- 标头:
Content-Type: application/json-patch+json
- 正文:从 JSON 项目文件夹中复制并粘贴其中一个 JSON 修补程序文档示例。