Nguon: Microsoft Learn · .NET 8.0

JsonPatch trong ASP.NET Core web API

Nguồn: JsonPatch in ASP.NET Core web API

Bài viết này giải thích cách xử lý các yêu cầu JSON Patch (vá lỗi JSON) trong ASP.NET Core web API.

Quan trọng: Tiêu chuẩn JSON Patch có những rủi ro bảo mật cố hữu. Việc triển khai này không cố gắng giảm thiểu những rủi ro bảo mật cố hữu này. Trách nhiệm của nhà phát triển là đảm bảo tài liệu JSON Patch an toàn để áp dụng lên đối tượng đích. Xem thêm phần Giảm thiểu rủi ro bảo mật.

Cài đặt package (gói)

Hỗ trợ JSON Patch trong ASP.NET Core web API dựa trên Newtonsoft.Json và yêu cầu gói NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

Để bật hỗ trợ JSON Patch:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddNewtonsoftJson();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

AddNewtonsoftJson thay thế các formatter (bộ định dạng) input và output dựa trên System.Text.Json mặc định được dùng để định dạng tất cả nội dung JSON. Extension method này tương thích với các phương thức đăng ký MVC service sau:

JsonPatch yêu cầu đặt header Content-Type thành application/json-patch+json.

Thêm hỗ trợ JSON Patch khi sử dụng System.Text.Json

Input formatter (bộ định dạng đầu vào) dựa trên System.Text.Json không hỗ trợ JSON Patch. Để thêm hỗ trợ JSON Patch sử dụng Newtonsoft.Json, trong khi vẫn giữ nguyên các formatter input và output khác:

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

Đoạn code trên tạo một instance (thực thể) của NewtonsoftJsonPatchInputFormatter và chèn nó vào vị trí đầu tiên trong collection MvcOptions.InputFormatters. Thứ tự đăng ký này đảm bảo rằng:

Dùng phương thức Newtonsoft.Json.JsonConvert.SerializeObject để serialize (tuần tự hóa) JsonPatchDocument.

Phương thức HTTP request PATCH

Phương thức PUT và PATCH được dùng để cập nhật tài nguyên hiện có. Sự khác biệt giữa chúng là PUT thay thế toàn bộ tài nguyên, trong khi PATCH chỉ chỉ định những thay đổi cần thiết.

JSON Patch

JSON Patch là một định dạng để chỉ định các cập nhật sẽ được áp dụng lên một tài nguyên. Một tài liệu JSON Patch có một mảng các operation (thao tác). Mỗi operation xác định một loại thay đổi cụ thể. Ví dụ về các thay đổi như vậy bao gồm thêm một phần tử mảng hoặc thay thế giá trị thuộc tính.

Ví dụ, các tài liệu JSON sau đây biểu diễn một tài nguyên, một tài liệu JSON Patch cho tài nguyên, và kết quả sau khi áp dụng các Patch operation.

Ví dụ về tài nguyên

json
{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}

Ví dụ về JSON patch

json
[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

Trong JSON trên:

Tài nguyên sau khi patch

Đây là tài nguyên sau khi áp dụng tài liệu JSON Patch trên:

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

Các thay đổi được thực hiện bằng cách áp dụng tài liệu JSON Patch lên tài nguyên là nguyên tử (atomic). Nếu bất kỳ operation nào trong danh sách thất bại, không có operation nào trong danh sách được áp dụng.

Cú pháp Path (đường dẫn)

Thuộc tính path của object operation có dấu gạch chéo giữa các cấp. Ví dụ: "/address/zipCode".

Index (chỉ số) dựa trên 0 được dùng để chỉ định các phần tử mảng. Phần tử đầu tiên của mảng addresses sẽ ở /addresses/0. Để add (thêm) vào cuối mảng, dùng dấu gạch ngang (-) thay vì số index: /addresses/-.

Các operation

Bảng sau đây hiển thị các operation được hỗ trợ theo định nghĩa trong đặc tả JSON Patch:

OperationGhi chú
addThêm thuộc tính hoặc phần tử mảng. Với thuộc tính đã có: đặt giá trị.
removeXóa thuộc tính hoặc phần tử mảng.
replaceTương đương remove theo sau là add tại cùng vị trí.
moveTương đương remove từ nguồn theo sau là add đến đích sử dụng giá trị từ nguồn.
copyTương đương add đến đích sử dụng giá trị từ nguồn.
testTrả về status code thành công nếu giá trị tại path = value được cung cấp.

JSON Patch trong ASP.NET Core

Việc triển khai JSON Patch trong ASP.NET Core được cung cấp trong gói NuGet Microsoft.AspNetCore.JsonPatch.

Code của action method

Trong API controller, một action method (phương thức hành động) cho JSON Patch:

Đây là ví dụ:

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

Code này từ ứng dụng mẫu hoạt động với model Customer sau:

csharp
namespace JsonPatchSample.Models;

public class Customer
{
    public string? CustomerName { get; set; }
    public List<Order>? Orders { get; set; }
}
csharp
namespace JsonPatchSample.Models;

public class Order
{
    public string OrderName { get; set; }
    public string OrderType { get; set; }
}

Action method mẫu:

Trong ứng dụng thực tế, code sẽ lấy dữ liệu từ kho lưu trữ như cơ sở dữ liệu và cập nhật cơ sở dữ liệu sau khi áp dụng patch.

Model state (trạng thái model)

Ví dụ action method trên gọi overload (quá tải) của ApplyTo nhận model state làm một trong các tham số. Với tùy chọn này, bạn có thể nhận thông báo lỗi trong response. Ví dụ sau hiển thị body của response 400 Bad Request cho một test operation:

json
{
  "Customer": [
    "The current value 'John' at path 'customerName' != test value 'Nancy'."
  ]
}

Dynamic object (đối tượng động)

Ví dụ action method sau đây cho thấy cách áp dụng patch lên một dynamic object:

csharp
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}

Operation add

Tài liệu patch mẫu sau đặt giá trị của CustomerName và thêm object Order vào cuối mảng Orders.

json
[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

Operation remove

Tài liệu patch mẫu sau đặt CustomerName thành null và xóa Orders[0]:

json
[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]

Operation replace

Operation này về mặt chức năng giống với remove theo sau là add.

Tài liệu patch mẫu sau đặt giá trị của CustomerName và thay thế Orders[0] bằng một object Order mới:

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

Operation move

Tài liệu patch mẫu sau:

json
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

Operation copy

Operation này về mặt chức năng giống với operation move nhưng không có bước remove cuối cùng.

Tài liệu patch mẫu sau:

json
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

Operation test

Nếu giá trị tại vị trí được chỉ định bởi path khác với giá trị được cung cấp trong value, yêu cầu thất bại. Trong trường hợp đó, toàn bộ yêu cầu PATCH thất bại ngay cả khi tất cả các operation khác trong patch document sẽ thành công.

Operation test thường được dùng để ngăn cập nhật khi có xung đột đồng thời (concurrency conflict).

Tài liệu patch mẫu sau không có hiệu lực nếu giá trị ban đầu của CustomerName là "John", vì test thất bại:

json
[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]

Lấy code mẫu

Xem hoặc tải xuống code mẫu. (Cách tải xuống).

Để kiểm tra mẫu, chạy ứng dụng và gửi HTTP requests với các cài đặt sau:

Giảm thiểu rủi ro bảo mật

Khi sử dụng gói Microsoft.AspNetCore.JsonPatch với triển khai dựa trên Newtonsoft.Json, điều quan trọng là phải hiểu và giảm thiểu các rủi ro bảo mật tiềm ẩn. Các phần sau đây phác thảo các rủi ro bảo mật liên quan đến JSON Patch và cung cấp các biện pháp giảm thiểu được khuyến nghị để đảm bảo sử dụng gói một cách an toàn.

Quan trọng: Đây không phải là danh sách đầy đủ các mối đe dọa. Các nhà phát triển ứng dụng phải tự thực hiện đánh giá mô hình mối đe dọa riêng để xác định danh sách toàn diện cụ thể cho ứng dụng và đưa ra các biện pháp giảm thiểu phù hợp. Ví dụ, các ứng dụng cho phép thao tác patch trên collection nên xem xét khả năng tấn công độ phức tạp thuật toán nếu các thao tác đó chèn hoặc xóa phần tử ở đầu collection.

Từ chối dịch vụ (DoS) thông qua khuếch đại bộ nhớ

csharp
public void Validate(JsonPatchDocument patch)
{
    // Đây chỉ là ví dụ. Tùy thuộc vào nhà phát triển để đảm bảo
    // trường hợp này được xử lý đúng cách, dựa trên nhu cầu ứng dụng.
    if (patch.Operations.Where(op => op.OperationType == OperationType.Copy).Count()
                              > MaxCopyOperationsCount)
    {
        throw new InvalidOperationException();
    }
}

Phá hoại logic nghiệp vụ (Business Logic Subversion)

Xác thực và phân quyền (Authentication and authorization)