共用方式為


ASP.NET Core Web API 中的 JSON Patch 支援

本文說明如何處理 ASP.NET Core Web API 中的 JSON Patch 要求。

ASP.NET Core Web API 中的 JSON 補丁支援依賴於System.Text.Json 的序列化,而且需要 Microsoft.AspNetCore.JsonPatch.SystemTextJson NuGet 套件。

什麼是 JSON 補丁標準?

JSON Patch 標準:

  • 這是描述要套用至 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 Patch 支援

ASP.NET Core Web API 中的 JSON Patch 支援是基於System.Text.Json序列化的,從 .NET 10 開始,Microsoft.AspNetCore.JsonPatch 的實作是基於System.Text.Json序列化。 這項功能:

Note

Microsoft.AspNetCore.JsonPatch串行化為基礎的System.Text.Json實作並不能直接取代舊版的Newtonsoft.Json實作。 它不支援動態類型,例如 ExpandoObject

Important

JSON Patch 標準具有 固有的安全性風險。 由於這些風險固有於 JSON Patch 標準,因此 ASP.NET Core 實作 不會嘗試降低固有的安全性風險。 開發人員有責任確保 JSON Patch 檔安全地套用至目標物件。 如需詳細資訊,請參閱 減輕安全性風險 一節。

啟用 JSON Patch 支援System.Text.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 補丁的方法代碼

在 API 控制器中,JSON Patch 的動作方法:

範例控制器動作方法:

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

範例動作方法的主要步驟:

  • 取得客戶
    • 方法會使用提供的標識碼,從資料庫Customer擷取 AppDb 物件。
    • 如果找不到 Customer 物件,則會傳 404 Not Found 回回應。
  • 套用 JSON 修補程式
    • 方法 ApplyTo(Object) 會將來自 patchDoc 的 JSON Patch 作業套用至擷取 Customer 的物件。
    • 如果在修補應用程式期間發生錯誤,例如無效的作業或衝突,則會由錯誤處理委派擷取這些錯誤。 此委派會將錯誤訊息以受影響物件的型別名稱和錯誤訊息形式,新增至 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 Patch 檔套用至 物件。

範例:將 套用 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 作業,它會檢查指定的值是否等於目標屬性。 如果沒有,則會傳回錯誤。

下列範例示範如何正常處理這些錯誤。

Important

傳遞至 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 Patch 相關聯的已識別安全性風險,並提供建議的防護功能,以確保套件的安全使用。

Important

這不是完整的威脅清單。 應用程式開發人員必須進行自己的威脅模型檢閱,以判斷應用程式專屬的完整清單,並視需要提出適當的風險降低措施。 例如,提供集合修補作業的應用程式應該考慮演算法複雜度攻擊的可能性,尤其是在這些作業於集合開頭插入或移除元素時。

若要將 JSON Patch 功能整合到其應用程式中時將安全性風險降到最低,開發人員應該:

  • 針對自己的應用程式執行完整的威脅模型。
  • 解決已識別的威脅。
  • 請遵循下列各節中建議的緩和措施。

透過記憶體放大進行拒絕服務攻擊(DoS)

  • 案例:惡意用戶端會提交 copy 多次重複大型物件圖形的作業,導致記憶體耗用量過多。
  • 影響:潛在的Of-Memory 超限(OOM)狀況,導致服務中斷。
  • Mitigation:
    • 在呼叫 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();
    }
}

商業邏輯顛覆

  • 案例:修補操作可能操作具有隱含不變量的欄位(例如,內部旗標、ID,或計算欄位),違反業務限制。
  • 影響:數據完整性問題和非預期的應用程序行為。
  • Mitigation:
    • 使用 POCO(簡單舊有的 CLR 物件),這些物件擁有明確定義且可安全修改的屬性。
      • 避免在目標對象中公開敏感性或安全性關鍵屬性。
      • 如果未使用 POCO 物件,請在套用操作後驗證修補的物件,以確保不會違反商務規則和不變式。

身份驗證與授權

  • 案例:未驗證或未授權的客戶端傳送惡意 JSON Patch 請求。
  • 影響:未經授權存取以修改敏感數據或中斷應用程式行為。
  • Mitigation:
    • 使用適當的驗證和授權機制保護接受 JSON 修補程式要求的端點。
    • 限制對具有適當許可權的受信任客戶端或使用者的存取。

取得程式碼

檢視或下載範例程式碼。 (如何下載)。

若要測試範例,請執行應用程式,並使用下列設定來傳送 HTTP 要求:

  • URL:http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • HTTP 方法:PATCH
  • 標題:Content-Type: application/json-patch+json
  • 內文:從 JSON 專案資料夾中複製並貼上其中一個 JSON 修補文件範例。

其他資源

本文說明如何處理 ASP.NET Core Web API 中的 JSON Patch 要求。

Important

JSON Patch 標準具有 固有的安全性風險。 此實作 不會嘗試降低這些固有的安全性風險。 開發人員有責任確保 JSON Patch 檔安全地套用至目標物件。 如需詳細資訊,請參閱 減輕安全性風險 一節。

套件安裝

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 修補程式

JSON Patch \(英文\) 是一種格式,可用來指定要套用至資源的更新。 JSON Patch 文件具有一個「作業」陣列。 每個作業都會識別特定類型的變更。 這類變更的範例包括新增陣列元素或取代屬性值。

例如,下列 JSON 文件代表一個資源、一份適用於該資源的 JSON 修補文件,以及套用修補作業的結果。

資源範例

{
  "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 Patch 文件之後的資源:

{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}

將 JSON 修補文件套用至資源所做的變更是不可部分完成的。 如果清單中有任何作業失敗,則不會套用清單中的任何作業。

路徑語法

作業物件的 path \(英文\) 屬性在層級之間有斜線。 例如: "/address/zipCode"

以零為起始的索引可用來指定陣列元素。 addresses 陣列的第一個元素會在 /addresses/0 上。 若要將 add 到陣列結尾處,請使用連字號 (-) 而不是索引號碼:/addresses/-

Operations

下表顯示支援的作業,如 JSON Patch 規格 \(英文\) 中所定義:

Operation Notes
add 加入屬性或陣列元素。 針對現有的屬性:設定值。
remove 移除屬性或陣列元素。
replace remove 之後接著在同一個位置上 add 相同。
move 與從來源 remove 之後接著使用來源的值 add 到目的地相同。
copy 與使用來源的值 add 到目的地相同。
test 如果 path 上的值 = 所提供的 value,即會傳回成功狀態碼。

ASP.NET Core 中的 JSON 修補檔

Microsoft.AspNetCore.JsonPatch \(英文\) NuGet 套件中會提供 JSON Patch 的 ASP.NET Core 實作。

動作方法程式碼

在 API 控制器中,JSON Patch 的動作方法:

以下為範例:

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

取代作業

此作業在功能上與 remove 之後接著 add 相同。

下列範例修補文件會設定 CustomerName 的值,並使用新的 Orders[0] 物件來取代 Order

[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

移動作業

  • 如果 path 指向陣列元素:將 from 元素複製到 path 元素的位置,然後在 remove 元素上執行 from 作業。
  • 如果 path 指向屬性:將 from 屬性的值複製到 path 屬性,然後在 remove 屬性上執行 from 作業。
  • 如果 path 指向不存在的屬性:
    • 如果要修補的資源是靜態物件:要求失敗。
    • 如果要修補的資源是動態物件:將 from 屬性複製到 path 所指出的位置,然後在 remove 屬性上執行 from 作業。

下列範例修補文件:

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

複製作業

此作業在功能上與不含最後 move 步驟的 remove 作業相同。

下列範例修補文件:

  • 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 Patch 相關聯的已識別安全性風險,並提供建議的防護功能,以確保套件的安全使用。

Important

這不是完整的威脅清單。 應用程式開發人員必須進行自己的威脅模型檢閱,以判斷應用程式專屬的完整清單,並視需要提出適當的風險降低措施。 例如,提供集合修補作業的應用程式應該考慮演算法複雜度攻擊的可能性,尤其是在這些作業於集合開頭插入或移除元素時。

藉由針對自己的應用程式執行完整的威脅模型,並解決所識別的威脅,同時遵循下列建議的緩和措施,這些套件的取用者可以將 JSON Patch 功能整合到其應用程式中,同時將安全性風險降至最低。

透過記憶體放大進行拒絕服務攻擊(DoS)

  • 案例:惡意用戶端會提交 copy 多次重複大型物件圖形的作業,導致記憶體耗用量過多。
  • 影響:潛在的Of-Memory 超限(OOM)狀況,導致服務中斷。
  • Mitigation:
    • 在呼叫 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();
    }
}

商業邏輯顛覆

  • 案例:修補操作可能操作具有隱含不變量的欄位(例如,內部旗標、ID,或計算欄位),違反業務限制。
  • 影響:數據完整性問題和非預期的應用程序行為。
  • Mitigation:
    • 使用安全且可修改的屬性明確定義的 POCO 物件。
    • 避免在目標對象中公開敏感性或安全性關鍵屬性。
    • 如果未使用 POCO 物件,請在套用操作之後驗證修補的物件,以確保不會違反商業規則和不變條件。

身份驗證與授權

  • 案例:未驗證或未授權的客戶端傳送惡意 JSON Patch 請求。
  • 影響:未經授權存取以修改敏感數據或中斷應用程式行為。
  • Mitigation:
    • 使用適當的驗證和授權機制保護接受 JSON 修補程式要求的端點。
    • 限制對具有適當許可權的受信任客戶端或使用者的存取。

其他資源

本文說明如何處理 ASP.NET Core Web API 中的 JSON Patch 要求。

Important

JSON Patch 標準具有 固有的安全性風險。 由於這些風險固有於 JSON Patch 標準,因此此實作 不會嘗試降低固有的安全性風險。 開發人員有責任確保 JSON Patch 檔安全地套用至目標物件。 如需詳細資訊,請參閱 減輕安全性風險 一節。

套件安裝

若要在您的應用程式中啟用 JSON 修補檔支援,請完成下列步驟:

  1. 安裝 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 套件。

  2. 更新專案的 Startup.ConfigureServices 方法以呼叫 AddNewtonsoftJson。 例如:

    services
        .AddControllersWithViews()
        .AddNewtonsoftJson();
    

AddNewtonsoftJson 與 MVC 服務註冊方法相容:

JSON 修補檔、AddNewtonsoftJson 和 System.Text.Json

AddNewtonsoftJson 會取代用於格式化 System.Text.Json JSON 內容的 型輸入和輸出格式器。 若要使用 Newtonsoft.Json新增對 JSON 修補檔的支援,同時讓其他格式器保持不變,請更新專案的 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 修補程式

JSON Patch \(英文\) 是一種格式,可用來指定要套用至資源的更新。 JSON Patch 文件具有一個「作業」陣列。 每個作業都會識別特定類型的變更。 這類變更的範例包括新增陣列元素或取代屬性值。

例如,下列 JSON 文件代表一個資源、一份適用於該資源的 JSON 修補文件,以及套用修補作業的結果。

資源範例

{
  "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 Patch 文件之後的資源:

{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}

將 JSON 修補文件套用至資源所做的變更是不可部分完成的。 如果清單中有任何作業失敗,則不會套用清單中的任何作業。

路徑語法

作業物件的 path \(英文\) 屬性在層級之間有斜線。 例如: "/address/zipCode"

以零為起始的索引可用來指定陣列元素。 addresses 陣列的第一個元素會在 /addresses/0 上。 若要將 add 到陣列結尾處,請使用連字號 (-) 而不是索引號碼:/addresses/-

Operations

下表顯示支援的作業,如 JSON Patch 規格 \(英文\) 中所定義:

Operation Notes
add 加入屬性或陣列元素。 針對現有的屬性:設定值。
remove 移除屬性或陣列元素。
replace remove 之後接著在同一個位置上 add 相同。
move 與從來源 remove 之後接著使用來源的值 add 到目的地相同。
copy 與使用來源的值 add 到目的地相同。
test 如果 path 上的值 = 所提供的 value,即會傳回成功狀態碼。

ASP.NET Core 中的 JSON 修補檔

Microsoft.AspNetCore.JsonPatch \(英文\) NuGet 套件中會提供 JSON Patch 的 ASP.NET Core 實作。

動作方法程式碼

在 API 控制器中,JSON Patch 的動作方法:

  • 使用 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"
  }
]

取代作業

此作業在功能上與 remove 之後接著 add 相同。

下列範例修補文件會設定 CustomerName 的值,並使用新的 Orders[0] 物件來取代 Order

[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

移動作業

  • 如果 path 指向陣列元素:將 from 元素複製到 path 元素的位置,然後在 remove 元素上執行 from 作業。
  • 如果 path 指向屬性:將 from 屬性的值複製到 path 屬性,然後在 remove 屬性上執行 from 作業。
  • 如果 path 指向不存在的屬性:
    • 如果要修補的資源是靜態物件:要求失敗。
    • 如果要修補的資源是動態物件:將 from 屬性複製到 path 所指出的位置,然後在 remove 屬性上執行 from 作業。

下列範例修補文件:

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

複製作業

此作業在功能上與不含最後 move 步驟的 remove 作業相同。

下列範例修補文件:

  • 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 修補文件範例。