JsonPatch trong 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:
- Cài đặt gói NuGet
Microsoft.AspNetCore.Mvc.NewtonsoftJson. - Gọi AddNewtonsoftJson. Ví dụ:
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:
- Cài đặt gói NuGet
Microsoft.AspNetCore.Mvc.NewtonsoftJson. - Cập nhật
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();
}
}Đ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:
NewtonsoftJsonPatchInputFormatterxử lý các yêu cầu JSON Patch.- Các input và output formatter dựa trên
System.Text.Jsonhiện có xử lý tất cả các yêu cầu và phản hồi JSON khác.
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
{
"customerName": "John",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
}
]
}Ví dụ về JSON patch
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]Trong JSON trên:
- Thuộc tính
opcho biết loại operation. - Thuộc tính
pathcho biết phần tử cần cập nhật. - Thuộc tính
valuecung cấp giá trị mới.
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:
{
"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:
| Operation | Ghi chú |
|---|---|
add | Thêm thuộc tính hoặc phần tử mảng. Với thuộc tính đã có: đặt giá trị. |
remove | Xóa thuộc tính hoặc phần tử mảng. |
replace | Tương đương remove theo sau là add tại cùng vị trí. |
move | Tương đương remove từ nguồn theo sau là add đến đích sử dụng giá trị từ nguồn. |
copy | Tương đương add đến đích sử dụng giá trị từ nguồn. |
test | Trả 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:
- Được gắn annotation (chú thích)
HttpPatch. - Nhận tham số JsonPatchDocument<TModel>, thường với
[FromBody]. - Gọi ApplyTo(Object) trên patch document để áp dụng các thay đổi.
Đây là ví dụ:
[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:
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; }
}Action method mẫu:
- Khởi tạo một
Customer. - Áp dụng patch.
- Trả về kết quả trong body của response.
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:
{
"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:
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
dynamic obj = new ExpandoObject();
patch.ApplyTo(obj);
return Ok(obj);
}Operation add
- Nếu
pathtrỏ đến một phần tử mảng: chèn phần tử mới trước phần tử được chỉ định bởipath. - Nếu
pathtrỏ đến một thuộc tính: đặt giá trị thuộc tính. - Nếu
pathtrỏ đến vị trí không tồn tại: - Nếu tài nguyên cần patch là dynamic object: thêm thuộc tính.
- Nếu tài nguyên cần patch là static object: yêu cầu thất bại.
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.
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]Operation remove
- Nếu
pathtrỏ đến một phần tử mảng: xóa phần tử. - Nếu
pathtrỏ đến một thuộc tính: - Nếu tài nguyên cần patch là dynamic object: xóa thuộc tính.
- Nếu tài nguyên cần patch là static object:
- Nếu thuộc tính là nullable: đặt thành null.
- Nếu thuộc tính là non-nullable: đặt thành
default<T>.
Tài liệu patch mẫu sau đặt CustomerName thành null và xóa Orders[0]:
[
{
"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:
[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]Operation move
- Nếu
pathtrỏ đến một phần tử mảng: sao chép phần tửfromvào vị trí của phần tửpath, sau đó thực hiện operationremovetrên phần tửfrom. - Nếu
pathtrỏ đến một thuộc tính: sao chép giá trị thuộc tínhfromvào thuộc tínhpath, sau đó thực hiện operationremovetrên thuộc tínhfrom. - Nếu
pathtrỏ đến thuộc tính không tồn tại: - Nếu tài nguyên cần patch là static object: yêu cầu thất bại.
- Nếu tài nguyên cần patch là dynamic object: sao chép thuộc tính
fromvào vị trí được chỉ định bởipath, sau đó thực hiện operationremovetrên thuộc tínhfrom.
Tài liệu patch mẫu sau:
- Sao chép giá trị của
Orders[0].OrderNamevàoCustomerName. - Đặt
Orders[0].OrderNamethành null. - Di chuyển
Orders[1]đến trướcOrders[0].
[
{
"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:
- Sao chép giá trị của
Orders[0].OrderNamevàoCustomerName. - Chèn bản sao của
Orders[1]trướcOrders[0].
[
{
"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:
[
{
"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:
- URL:
http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate - Phương thức HTTP:
PATCH - Header:
Content-Type: application/json-patch+json - Body: Sao chép và dán một trong các ví dụ tài liệu JSON patch từ thư mục JSON của project.
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ớ
- Kịch bản: Một client độc hại gửi operation
copynhân bản các object graph lớn nhiều lần, dẫn đến tiêu thụ bộ nhớ quá mức. - Tác động: Có thể gây ra điều kiện Out-Of-Memory (OOM), gây gián đoạn dịch vụ.
- Biện pháp giảm thiểu:
- Xác thực tài liệu JSON Patch đến về kích thước và cấu trúc trước khi gọi
ApplyTo. - Việc xác thực cần phải cụ thể cho ứng dụng, nhưng một ví dụ xác thực có thể trông như sau:
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)
- Kịch bản: Các Patch operation có thể thao tác các trường có invariant (bất biến) ẩn (ví dụ: cờ nội bộ, ID, hoặc các trường tính toán), vi phạm ràng buộc nghiệp vụ.
- Tác động: Vấn đề toàn vẹn dữ liệu và hành vi ứng dụng không mong muốn.
- Biện pháp giảm thiểu:
- Sử dụng POCO (Plain Old CLR Object) với các thuộc tính được xác định rõ ràng an toàn để sửa đổi.
- Tránh expose (phơi bày) các thuộc tính nhạy cảm hoặc quan trọng về bảo mật trong đối tượng đích.
- Nếu không sử dụng POCO object, hãy xác thực đối tượng đã được patch sau khi áp dụng các operation để đảm bảo các quy tắc nghiệp vụ và invariant không bị vi phạm.
Xác thực và phân quyền (Authentication and authorization)
- Kịch bản: Các client chưa xác thực hoặc chưa được phân quyền gửi các yêu cầu JSON Patch độc hại.
- Tác động: Truy cập trái phép để sửa đổi dữ liệu nhạy cảm hoặc làm gián đoạn hành vi ứng dụng.
- Biện pháp giảm thiểu:
- Bảo vệ các endpoint nhận yêu cầu JSON Patch bằng các cơ chế xác thực và phân quyền phù hợp.
- Hạn chế quyền truy cập cho các client hoặc người dùng đáng tin cậy với quyền hạn phù hợp.