다음을 통해 공유


ASP.NET Core Web API의 JSON 패치 지원

이 문서에서는 ASP.NET Core Web API에서 JSON 패치 요청을 처리하는 방법을 설명합니다.

ASP.NET Core Web API에서 JSON 패치 지원은 System.Text.Json 직렬화를 기반으로 하며, Microsoft.AspNetCore.JsonPatch.SystemTextJson NuGet 패키지가 필요합니다.

JSON 패치 표준이란?

JSON 패치 표준:

  • JSON 문서에 적용할 변경 내용을 설명하는 표준 형식입니다.

  • RFC 6902에 정의되며 JSON 리소스에 대한 부분 업데이트를 수행하기 위해 RESTful API에서 널리 사용됩니다.

  • 다음과 같이 JSON 문서를 수정하는 작업 시퀀스를 설명합니다.

    • add
    • remove
    • replace
    • move
    • copy
    • test

웹앱에서 JSON 패치는 일반적으로 PATCH 작업에서 리소스의 부분 업데이트를 수행하는 데 사용됩니다. 클라이언트는 업데이트를 위해 전체 리소스를 보내는 대신 변경 내용만 포함하는 JSON 패치 문서를 보낼 수 있습니다. 패치를 사용하면 페이로드 크기가 줄어들고 효율성이 향상됩니다.

JSON 패치 표준에 대한 개요는 jsonpatch.com 참조하세요.

ASP.NET Core Web API의 JSON 패치 지원

ASP.NET Core Web API의 JSON 패치 지원은 .NET 10부터 System.Text.Json 직렬화를 기반으로 Microsoft.AspNetCore.JsonPatch을 구현하며 System.Text.Json 직렬화를 기반으로 합니다. 이 기능은 다음과 같습니다.

Note

Microsoft.AspNetCore.JsonPatch 기반 serialization 구현인 System.Text.Json는 레거시 Newtonsoft.Json 기반 구현의 직접적인 대체가 아닙니다. 예를 들어 ExpandoObject동적 형식은 지원하지 않습니다.

Important

JSON 패치 표준에는 고유한 보안 위험이 있습니다. 이러한 위험은 JSON 패치 표준에 내재되어 있으므로 ASP.NET Core 구현은 내재된 보안 위험을 완화하려고 시도하지 않습니다. JSON 패치 문서가 대상 개체에 안전하게 적용되도록 하는 것은 개발자의 책임입니다. 자세한 내용은 보안 위험 완화 섹션을 참조하세요 .

System.Text.Json로 JSON 패치 지원을 활성화하다

JSON 패치 지원을 System.Text.Json 사용하도록 설정하려면 Microsoft.AspNetCore.JsonPatch.SystemTextJson NuGet 패키지를 설치합니다.

dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease

이 패키지는 JsonPatchDocument<TModel> 형식의 개체에 대한 JSON 패치 문서를 나타내는 클래스와 T를 사용하여 JSON 패치 문서를 직렬화 및 역직렬화하기 위한 사용자 지정 논리를 제공합니다. JsonPatchDocument<TModel> 클래스의 핵심 메서드는 ApplyTo(Object) 형식의 대상 객체에 패치 작업을 적용하는 T입니다.

JSON 패치를 적용하는 작업 메서드 코드

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

샘플 앱의 이 코드는 다음 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를 Customer 사용하여 데이터베이스 AppDb 에서 개체를 검색합니다.
    • 개체Customer가 없으면 404 Not Found 응답을 반환합니다.
  • JSON 패치 적용:
    • 이 메서드는 ApplyTo(Object) patchDoc의 JSON 패치 작업을 검색된 Customer 개체에 적용합니다.
    • 잘못된 작업 또는 충돌과 같은 패치 애플리케이션 중에 오류가 발생하면 오류 처리 대리자로 캡처됩니다. 이 대리자는 영향을 받는 개체의 형식 이름과 오류 메시지를 사용하여 ModelState에 오류 메시지를 추가합니다.
  • ModelState 유효성 검사:
    • 패치를 적용한 후, 메서드는 ModelState의 오류를 확인합니다.
    • ModelState 패치 오류와 같이 잘못된 경우 유효성 검사 오류가 있는 400 Bad Request 응답을 반환합니다.
  • 업데이트된 고객을 반환합니다.
    • 패치가 성공적으로 적용되고 ModelState 유효한 경우 메서드는 응답에서 업데이트 Customer 된 개체를 반환합니다.

오류 응답 예제:

다음 예제에서는 지정된 경로가 400 Bad Request 잘못된 경우 JSON 패치 작업에 대한 응답 본문을 보여 줍니다.

{
  "Customer": [
    "The target location specified by path segment 'foobar' was not found."
  ]
}

개체에 JSON 패치 문서 적용

다음 예제에서는 메서드를 사용하여 개체에 ApplyTo(Object) JSON 패치 문서를 적용하는 방법을 보여 줍니다.

예: 개체에 JsonPatchDocument<TModel> 적용

아래 예제에서는 다음을 보여줍니다.

  • add, replaceremove 작업입니다.
  • 중첩된 속성에 대한 작업입니다.
  • 배열에 새 항목 추가
  • 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 패치와 관련된 식별된 보안 위험을 간략하게 설명하고 패키지의 안전한 사용을 보장하기 위한 권장 완화 방법을 제공합니다.

Important

이 목록은 전체 위협 목록이 아닙니다. 앱 개발자는 자체 위협 모델 검토를 수행하여 앱별 포괄적인 목록을 결정하고 필요에 따라 적절한 완화를 마련해야 합니다. 예를 들어 패치 작업에 컬렉션을 노출하는 앱은 해당 작업이 컬렉션의 시작 부분에 요소를 삽입하거나 제거하는 경우 알고리즘 복잡성 공격의 가능성을 고려해야 합니다.

JSON 패치 기능을 앱에 통합할 때 보안 위험을 최소화하려면 개발자는 다음을 수행해야 합니다.

  • 자체 앱에 대해 포괄적인 위협 모델을 실행합니다.
  • 식별된 위협을 해결합니다.
  • 다음 섹션의 권장 완화를 따릅니다.

메모리 증폭을 통한 DoS(서비스 거부)

  • 시나리오: 악의적인 클라이언트가 copy 큰 개체 그래프를 여러 번 복제하여 과도한 메모리 소비를 초래하는 작업을 제출합니다.
  • 영향: OOM(잠재적 아웃-Of-Memory) 조건으로 인해 서비스 중단이 발생합니다.
  • 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 패치 요청을 보냅니다.
  • 영향: 중요한 데이터를 수정하거나 앱 동작을 방해하는 무단 액세스입니다.
  • 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 패치 요청을 처리하는 방법을 설명합니다.

Important

JSON 패치 표준에는 고유한 보안 위험이 있습니다. 이 구현 은 이러한 내재된 보안 위험을 완화하려고 시도하지 않습니다. JSON 패치 문서가 대상 개체에 안전하게 적용되도록 하는 것은 개발자의 책임입니다. 자세한 내용은 보안 위험 완화 섹션을 참조하세요 .

패키지 설치

ASP.NET Core 웹 API의 JSON 패치 지원은 NuGet 패키지를 기반으로 Newtonsoft.Json 하며 필요합니다 Microsoft.AspNetCore.Mvc.NewtonsoftJson .

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데 사용되는 기본 -based 입력 및 출력 포맷터를 대체합니다. 이 확장 메서드는 다음 MVC 서비스 등록 방법과 호환됩니다.

JsonPatch를 사용하려면 헤더를 Content-Type .로 설정해야 합니다 application/json-patch+json.

System.Text.Json을 사용할 때 JSON 패치에 대한 지원 추가

System.Text.Json기반 입력 포맷터는 JSON 패치를 지원하지 않습니다. 다른 입력 및 출력 포맷터를 변경하지 않고 사용하여 JSON 패치 Newtonsoft.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 패치는 리소스에 적용할 업데이트를 지정하기 위한 형식입니다. JSON 패치 문서에는 작업 배열이 있습니다. 각 작업은 특정 유형의 변경 내용을 식별합니다. 이러한 변경의 예제로는 배열 요소 추가 또는 속성 값 바꾸기가 있습니다.

예를 들어 다음 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 패치 문서를 적용한 후 리소스는 다음과 같습니다.

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

JSON 패치 문서를 리소스에 적용하여 변경한 내용은 원자성입니다. 목록에서 작업이 실패하면 목록의 작업이 적용되지 않습니다.

경로 구문

작업 개체의 path 속성에서 수준 사이에는 슬래시가 있습니다. 예들 들어 "/address/zipCode"입니다.

0부터 시작하는 인덱스는 배열 요소를 지정하는 데 사용됩니다. addresses 배열의 첫 번째 요소는 /addresses/0에 있습니다. 배열 끝에 add(추가)하려면 인덱스 번호가 아닌 하이픈(-)을 사용합니다(/addresses/-).

Operations

다음 표에서는 JSON 패치 사양에 정의된 지원되는 작업을 보여 줍니다.

Operation Notes
add 속성 또는 배열 요소를 추가합니다. 기존 속성의 경우 값을 설정합니다.
remove 속성 또는 배열 요소를 제거합니다.
replace 동일한 위치에서 remove가 뒤에 오는 add와 같습니다.
move 소스의 값을 사용하는 대상에 대한 remove가 뒤에 오는 소스에서 add와 같습니다.
copy 소스의 값을 사용하는 대상에 대한 add와 같습니다.
test path의 값이 제공된 value와 같은 경우 성공 상태 코드를 반환합니다.

ASP.NET Core의 JSON 패치

JSON 패치의 ASP.NET Core 구현은 Microsoft.AspNetCore.JsonPatch NuGet 패키지로 제공됩니다.

작업 메서드 코드

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 :
    • 패치할 리소스가 동적 개체인 경우: 속성을 제거합니다.
    • 패치할 리소스가 정적 개체인 경우:
      • 속성이 nullable인 경우: null로 설정합니다.
      • 속성이 nullable이 아닌 경우: 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 패치와 관련된 식별된 보안 위험을 간략하게 설명하고 패키지의 안전한 사용을 보장하기 위한 권장 완화 방법을 제공합니다.

Important

이 목록은 전체 위협 목록이 아닙니다. 앱 개발자는 자체 위협 모델 검토를 수행하여 앱별 포괄적인 목록을 결정하고 필요에 따라 적절한 완화를 마련해야 합니다. 예를 들어 패치 작업에 컬렉션을 노출하는 앱은 해당 작업이 컬렉션의 시작 부분에 요소를 삽입하거나 제거하는 경우 알고리즘 복잡성 공격의 가능성을 고려해야 합니다.

자체 앱에 대해 포괄적인 위협 모델을 실행하고 아래 권장 완화를 따르는 동안 식별된 위협을 해결함으로써 이러한 패키지의 소비자는 보안 위험을 최소화하면서 JSON 패치 기능을 앱에 통합할 수 있습니다.

메모리 증폭을 통한 DoS(서비스 거부)

  • 시나리오: 악의적인 클라이언트가 copy 큰 개체 그래프를 여러 번 복제하여 과도한 메모리 소비를 초래하는 작업을 제출합니다.
  • 영향: OOM(잠재적 아웃-Of-Memory) 조건으로 인해 서비스 중단이 발생합니다.
  • 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 패치 요청을 보냅니다.
  • 영향: 중요한 데이터를 수정하거나 앱 동작을 방해하는 무단 액세스입니다.
  • Mitigation:
    • 적절한 인증 및 권한 부여 메커니즘을 사용하여 JSON 패치 요청을 수락하는 엔드포인트를 보호합니다.
    • 적절한 권한이 있는 신뢰할 수 있는 클라이언트 또는 사용자에 대한 액세스를 제한합니다.

추가 리소스

이 문서에서는 ASP.NET Core Web API에서 JSON 패치 요청을 처리하는 방법을 설명합니다.

Important

JSON 패치 표준에는 고유한 보안 위험이 있습니다. 이러한 위험은 JSON 패치 표준에 내재되어 있으므로 이 구현은 내재된 보안 위험을 완화하려고 시도하지 않습니다. JSON 패치 문서가 대상 개체에 안전하게 적용되도록 하는 것은 개발자의 책임입니다. 자세한 내용은 보안 위험 완화 섹션을 참조하세요 .

패키지 설치

앱에서 JSON 패치 지원을 사용하도록 설정하려면 다음 단계를 완료합니다.

  1. Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 패키지를 설치합니다.

  2. 프로젝트의 Startup.ConfigureServices 메서드를 업데이트하여 AddNewtonsoftJson을 호출합니다. 다음은 그 예입니다.

    services
        .AddControllersWithViews()
        .AddNewtonsoftJson();
    

AddNewtonsoftJson은 MVC 서비스 등록 메서드

JSON Patch, AddNewtonsoftJson 및 System.Text.Json과 호환됩니다.

AddNewtonsoftJsonSystem.Text.Json 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 패치

JSON 패치는 리소스에 적용할 업데이트를 지정하기 위한 형식입니다. JSON 패치 문서에는 작업 배열이 있습니다. 각 작업은 특정 유형의 변경 내용을 식별합니다. 이러한 변경의 예제로는 배열 요소 추가 또는 속성 값 바꾸기가 있습니다.

예를 들어 다음 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 패치 문서를 적용한 후 리소스는 다음과 같습니다.

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

JSON 패치 문서를 리소스에 적용하여 변경한 내용은 원자성입니다. 목록에서 작업이 실패하면 목록의 작업이 적용되지 않습니다.

경로 구문

작업 개체의 path 속성에서 수준 사이에는 슬래시가 있습니다. 예들 들어 "/address/zipCode"입니다.

0부터 시작하는 인덱스는 배열 요소를 지정하는 데 사용됩니다. addresses 배열의 첫 번째 요소는 /addresses/0에 있습니다. 배열 끝에 add(추가)하려면 인덱스 번호가 아닌 하이픈(-)을 사용합니다(/addresses/-).

Operations

다음 표에서는 JSON 패치 사양에 정의된 지원되는 작업을 보여 줍니다.

Operation Notes
add 속성 또는 배열 요소를 추가합니다. 기존 속성의 경우 값을 설정합니다.
remove 속성 또는 배열 요소를 제거합니다.
replace 동일한 위치에서 remove가 뒤에 오는 add와 같습니다.
move 소스의 값을 사용하는 대상에 대한 remove가 뒤에 오는 소스에서 add와 같습니다.
copy 소스의 값을 사용하는 대상에 대한 add와 같습니다.
test path의 값이 제공된 value와 같은 경우 성공 상태 코드를 반환합니다.

ASP.NET Core의 JSON 패치

JSON 패치의 ASP.NET Core 구현은 Microsoft.AspNetCore.JsonPatch NuGet 패키지로 제공됩니다.

작업 메서드 코드

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 :
    • 패치할 리소스가 동적 개체인 경우: 속성을 제거합니다.
    • 패치할 리소스가 정적 개체인 경우:
      • 속성이 nullable인 경우: null로 설정합니다.
      • 속성이 nullable이 아닌 경우: 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 패치 문서 샘플 중 하나를 복사하여 붙여넣습니다.