ASP.NET Core Web API 中对 JSON Patch 的支持

本文介绍如何处理 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。 此功能:

注释

基于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 修补程序的操作方法:

控制器动作方法示例:

[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);
}

示例应用中的此代码适用于以下 CustomerOrder 模型:

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
  • 应用 JSON 修补程序
    • ApplyTo(Object)方法对检索到的Customer对象应用来自patchDoc中的JSON Patch操作。
    • 如果在应用修补程序期间发生错误(例如无效操作或冲突),错误处理委托会捕获错误。 此委托使用受影响对象的类型名称和错误消息将错误消息添加到 ModelState
  • 验证 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>

下面的示例展示了如何:

  • addreplaceremove 操作。
  • 对嵌套属性的操作。
  • 向数组添加新项。
  • 在 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>,包括由以下选项控制的行为:

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 对象,请在应用操作后验证修补的对象,以确保不违反业务规则和不变量。

身份验证和授权

  • 场景:未经身份验证或未经授权的客户端发送恶意 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 与在相同位置后跟 addremove 相同。
move 与从后跟 add 的源到使用源中的值的目标的 remove 相同。
copy 与到使用源中的值的目标的 add 相同。
test 如果 path 处的值 = 提供的 value,则返回成功状态代码。

ASP.NET Core 中的 JSON Patch

Microsoft.AspNetCore.JsonPatch NuGet 包中提供了 JSON 修补程序的 ASP.NET Core 实现。

操作方法代码

在 API 控制器中,JSON 修补程序的操作方法:

下面是一个示例:

[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"
  }
]

替换操作

此操作在功能上与后跟 addremove 相同。

以下示例修补程序文档设置 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 支持,请完成以下步骤:

  1. 安装 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包。

  2. 更新项目的 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 与在相同位置后跟 addremove 相同。
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"
  }
]

替换操作

此操作在功能上与后跟 addremove 相同。

以下示例修补程序文档设置 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 修补程序文档示例