.NET/ASP.NET

ASP.NET Core - 16. Forms Tag Helper

클리엘 2023. 1. 27. 21:58
728x90

내장 Tag Helper 중에는 HTML Form을 만들기 위해 사용되는 것들도 존재합니다. 이들 tag helper는 form이 지정한 action이나 page handler method로 submit 되도록 하며 요소들을 정확하게 지정한 model 속성으로 표현되도록 합니다.

 

1. Project 준비하기

 

예제 Project는 이전글에서 사용하던 것을 그대로 사용할 것입니다. 다만 Views/Shared folder에 있는 _SimpleLayout.cshtml file을 아래와 같이 변경합니다.

<!DOCTYPE html>
<html>
<head>
	<title>@ViewBag.Title</title>
	<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
	<div class="m-2">
		@RenderBody()
	</div>
</body>
</html>

또한 유사한 content를 제공하기 위해 controller view와 razor page를 같이 사용할 것입니다. 때문에 controller와 page사이를 더 쉽게 구분하기 위해 Program.cs에 아래와 같이 route를 추가합니다.

app.UseStaticFiles();
//app.MapControllers();
//app.MapDefaultControllerRoute();
app.MapControllerRoute("forms", "controllers/{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

위 route는 URL이 controller를 대상으로 한다는 것을 명확하게 하는 static path segment를 사용하였습니다.

 

project를 실행하고 /controllers/home/list URL을 요청하여 다음과 같은 응답이 생성되는지 확인합니다.

2. Form Handling Pattern

 

대부분의 HTML Form에는 다음과 같이 잘 정의된 pattern이 존재합니다. browser는 HTTP Get요청을 보내어 form을 포함한 HTML응답을 생성하게 되고 이것으로 사용자가 application에 data를 제공할 수 있도록 합니다. 그리고 사용자는 HTTP Post요청을 통해 form data를 submit 하는 button을 click 하면 application은 전송된 data를 수신받아 해당 사용자 data를 처리하게 됩니다. 마지막으로 data가 잘 처리되고 나면 사용자의 요청을 확인하는 URL로 browser를 redirection 하는 응답을 전송합니다.

이와 같은 처리는 Post/Redirect/Get pattern으로 알려진 것입니다. 특히 redirection은 사용자가 다른 POST요청을 보내지 않고 무심코 browser의 새로고침 button을 click하여 반복적인 동작을 유발할 수도 있으므로 중요하다고 할 수 있습니다. 이제 controller와 razor page를 통해 어떻게 pattern을 적용할 수 있는지를 살펴보도록 하겠습니다. 우선 pattern의 기본적인 구현을 시작으로 tag helper와 model binding기능을 접목하여 한 단계 개선된 service를 완성해 볼 것입니다.

 

(1) Form 처리를 위한 Controller 만들기

 

Form을 처리하는 Controller는 몇가지 기능을 결합함으로써 생성됩니다. Controllers folder에 FormController.cs이름의 file을 아래와 같이 추가합니다.

using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;

namespace MyWebApp.Controllers
{
    public class FormController : Controller
    {
        private NorthwindContext context;

        public FormController(NorthwindContext dbContext)
        {
            context = dbContext;
        }

        public async Task<IActionResult> Index(int id = 1)
        {
            return View("Form", await context.Products.FindAsync(id));
        }

        [HttpPost]
        public IActionResult SubmitForm()
        {
            foreach (string key in Request.Form.Keys.Where(k => !k.StartsWith("_")))
            {
                TempData[key] = string.Join(", ", Request.Form[key]);
            }

            return RedirectToAction(nameof(Results));
        }

        public IActionResult Results()
        {
            return View();
        }
    }
}

Index action method에서는 사용자에게 HTML Form을 Render 할 Form이름의 VIew를 지정하고 있습니다. 사용자가 form을 submit 하면 오로지 POST요청만 수신하도록 HttpPost attribute가 적용된 SubmitForm action으로 Data가 전달됩니다. 해당 action method는 HttpRequest.Form속성을 통해 HTML form data를 처리하고 temp data기능을 통해 data를 저장하고 있습니다. 각 form data값은 string배열로서 취급되고 있는데 저장을 위해 comma(,)로 분리된 단일 문자열로 변환합니다. 그리고 browser는 Results action method로 redirect를 수행해 View를 표시하게 됩니다.

예제에서는 밑줄(_)로 시작하지 않는 이름의 form data 값만을 표시하도록 하고 있습니다. 이렇게 하는 이유는 잠시 후 설명할 것입니다.

controller에 view를 제공하기 위해 Views/Form folder를 생성하고 Form.cshtml 이름의 razor view file을 아래와 같이 추가합니다.

@model Products
@{
	Layout = "_SimpleLayout";
}

<h5 class="bg-primary text-white text-center p-2">HTML Form</h5>

<form action="/controllers/form/submitform" method="post">
	<div class="form-group">
		<label>Name</label>
		<input class="form-control" name="Name" value="@Model?.ProductName" />
	</div>

	<button type="submit" class="btn btn-primary mt-2">Submit</button>
</form>

예제의 view는 간단한 HTML form을 포함하고 있는 것으로 POST요청을 통해 SubmitForm action method로 data를 Submit하도록 구성되었습니다. form에서는 값을 razor표현식을 통해 설정하는 input요소를 하나 포함하고 있습니다. 이어서 Views/Form folder에 Results.cshtml 이름의 razor view를 아래와 같이 추가합니다.

@{
	Layout = "_SimpleLayout";
}

<table class="table table-striped table-bordered table-sm">
	<thead>
		<tr class="bg-primary text-white text-center">
			<th colspan="2">Form Data</th>
		</tr>
	</thead>
	<tbody>
		@foreach (string key in TempData.Keys)
		{
			<tr>
				<th>@key</th>
				<td>@TempData[key]</td>
			</tr>
		}
	</tbody>
</table>
<a class="btn btn-primary" asp-action="Index">Return</a>

위 예제는 사용자에게 form data를 다시 표시하는 view입니다. 추후에 더 유용한 방법으로 form data가 어떻게 처리되는지를 볼 기회가 있겠지만 지금은 form을 생성하는데만 집중할 것이며 form에 포함된 data를 보는 것에 집중하도록 할 것입니다.

 

project를 실행한 뒤 HTML form을 보기위해 /controllers/form URL을 요청합니다. 그리고 text field에 값을 입력하고 SubmitForm action에 의해 처리될 POST요청을 submit button을 click 하여 전송합니다. 그러면 form data는 temp daa로 저장되고 browser가 redirection을 수행하여 다음과 같은 응답을 생성하게 될 것입니다.

(2) Form 처리를 위한 Razor Page 만들기

 

위 예제와 같은 pattern은 Razor Page에도 구현될 수 있습니다. 하나의 Page는 form화면을 표시하고 form data를 처리하며 다른 하나는 결과를 표시하는데 필요합니다. FormHandler.cshtml 이름의 Razor Page를 Pages folder에 아래와 같이 추가합니다.

@page "/pages/form/{id:long?}"
@model FormHandlerModel
@using Microsoft.AspNetCore.Mvc.RazorPages

<div class="m-2">
	<h5 class="bg-primary text-white text-center p-2">HTML Form</h5>
	<form action="/pages/form" method="post">
		<div class="form-group">
			<label>Name</label>
			<input class="form-control" name="Name" value="@Model.Products?.ProductName" />
		</div>
		<button type="submit" class="btn btn-primary mt-2">Submit</button>
	</form>
</div>
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyWebApp.Models;

namespace MyWebApp.Pages
{
	[IgnoreAntiforgeryToken]
	public class FormHandlerModel : PageModel
    {
		private NorthwindContext context;

		public FormHandlerModel(NorthwindContext dbContext)
		{
			context = dbContext;
		}
		
		public Products? Products { get; set; }

		public async Task OnGetAsync(int id = 1)
		{
			Products = await context.Products.FindAsync(id);
		}

		public IActionResult OnPost()
		{
			foreach (string key in Request.Form.Keys.Where(k => !k.StartsWith("_")))
				TempData[key] = string.Join(", ", Request.Form[key]);

			return RedirectToPage("FormResults");
		}
	}
}

OnGetAsync handler method는 Products를 database로 부터 가져오는데 이때 Products는 HTML form에서 input요소에 값을 설정하기 위해 view에서 사용됩니다. form은 OnPost handler method에 의해 처리될 HTTP Post요청을 전송하도록 구성되었으며 form data는 Temp data로 저장된 뒤 FormResults이름의 Razor Page로 Recirection을 전송하게 됩니다. browser가 redirect 될 FormResults.cshtml이름의 Razor Page를 Pages folder에 아래와 같이 추가합니다.

@page "/pages/results"

<div class="m-2">
	<table class="table table-striped table-bordered table-sm">
		<thead>
			<tr class="bg-primary text-white text-center">
				<th colspan="2">Form Data</th>
			</tr>
		</thead>
		<tbody>
			@foreach (string key in TempData.Keys)
			{
				<tr>
					<th>@key</th>
					<td>@TempData[key]</td>
				</tr>
			}
		</tbody>
	</table>
	<a class="btn btn-primary" asp-page="FormHandler">Return</a>
</div>

FormResults.cshtml에서 필요한 back-end code는 없으며 view에서 직접적으로 temp data에 접근하여 table에 표시하고 있습니다. project를 실행하고 /pages/form URL을 호출하여 input field에 값을 입력하고 submit button을 눌러줍니다. form data는 위 예제에서 정의된 OnPost method에 의해 처리되고 browser는 /pages/results로 redirect를 수행하여 다음과 같은 결과를 표시하게 될 것입니다.

만약 예제에서 RuntimeBinderException예외가 발생한다면 browser의 cookie를 삭제하고 다시 시도해보시기 바랍니다.

3. Tag Helper를 사용해 HTML Form개선하기

 

이전 예제에서는 HTML form을 처리하는 기본적인 mechanism에 대해 알아보았는데 ASP.NET Core는 form요소를 변환하는 tag helper 역시 지원하고 있습니다.

 

(1) Form 요소

 

FormTagHelper class는 form요소를 위한 내장 tag helper이며 HTML Form의 구성을 관리하는데 사용되는 것으로 URL의 hard-code 없이 정확한 action 혹은 page handler를 설정할 수 있습니다. 아래표에서는 해당 tag helper에서 사용할 수 있는 속성들을 나열한 것입니다.

asp-controller 이 속성은 action attribute URL의 routing system에 대한 controller를 지정하는데 사용됩니다.
asp-action 이 속성은 action attribute URL의 routing system에 대한 action method를 지정하는데 사용됩니다.
asp-page 이 속성은 razor page의 이름을 지정하는데 사용됩니다.
asp-page-handler 이 속성은 요청을 처리하는데 사용 handler method의 이름을 지정하는데 사용됩니다. 
asp-route-* asp-route-으로 시작되는 이름의 attribute는 action attribute URL에 대한 추가적인 값을 설정하는데 사용됩니다. 예를 들어 asp-route-id라고 하면 routing system에 id segment값을 제공하기 위해 사용됩니다.
asp-route 이 속성은 action attribute의 URL을 생성하는데 사용될 route의 이름을 지정합니다.
asp-antiforgery 이 속성은 anti-forgery정보가 view에 추가될지의 여부를 지정하는데 사용됩니다. 
asp-fragment 이 속성은 생성된 URL을 위한 segment를 지정하는데 사용됩니다.

● Form 대상 설정

 

FormTagHelper는 form요소를 변환하며 URL을 직접 지정하지 않고도 action method 혹은 Razor page를 form의 대상으로 지정할 수 있습니다. 또 해당 tag helper에 의해 지원되는 속성은 이전에 예제에서 구현했었던 anchor 요소와 같은 방법으로 동작하며 attribute를 사용하여 ASP.NET Core routing system을 통해 URL을 생성하는데 도움이 되는 값을 제공합니다.

 

아래 예제는 tag helper를 적용하기 위해 Views/Form folder에 있는 Form.cshtml file의 form요소를 변경한 것입니다.

form요소가 method attribute없이 정의되었다면 tag helper는 browser가 HTML명세를 따르고 있고 HTTP GET요청을 사용해 form을 전송한다는 것을 가정하게 되므로 post값을 가진 form을 추가할 것이며 이는 form이 HTTP POST 요청을 통해 form을 submit 할 수 있음을 의미합니다. 결국 이러한 처리는 form이 submit 되는 방식을 분명히 하기 위해 method attribute가 항상 지정되도록 합니다.

<form asp-action="submitform" method="post">

asp-action attribute는 HTTP 요청을 받을 action의 이름을 지정하는데 사용되며 routing system은 anchor 요소에서 처럼 URL을 생성하는 데 사용됩니다. asp-controller attribute는 예제에서 사용되지 않았는데 이는 view를 render 한 controller가 URL에 사용될 것이기 때문입니다.

 

asp-page attribute는 또한 razor page를 아래 나타난 예제처럼 form의 대상으로서 선택하는데 사용됩니다. 해당 예제는 Pages folder에 있는 FormHandler.cshtml file의 form요소를 변경한 것입니다.

<form action="FormHandler" method="post">

project를 실행하고 /controllers/form으로 URL을 요청하여 되돌아온 HTML을 확인해 보면 다음과 같이 tag helper가 form요소에 action속성을 추가하였음을 알 수 있습니다.

<form method="post" action="/controllers/Form/submitform">

routing system은 지정된 action mehod를 대상으로 하는 URL을 생성하기위해 사용되는데 이로서 routing설정의 변화가 자동적으로 form URL에 반영될 수 있습니다. 다시 /pages/form으로 URL을 요청하여 HTML응답을 확인해 보면 form요소가 대상 page URL로 다음과 같이 변경되어 있음을 알 수 있습니다.

<form action="/pages/form" method="post">

(2) Form Button 변환

 

form을 전송하는 button은 form요소 밖에서도 정의될 수 있습니다. 이러한 경우 button은 form요소의 id attribute에 해당하는 값인 form attribute와 form의 대상 URL을 지정하는 formaction attribute를 가질 수 있습니다.

 

tag helper는 asp-action이나 asp-controller또는 asp-page를 통해 formaction attribute를 생성할 것입니다. 아래 예제는 Views/Form folder의 form.cshtml에서 button을 변환한 것을 나타내고 있습니다.

<form asp-action="submitform" method="post" id="htmlform">
	<div class="form-group">
		<label>Name</label>
		<input class="form-control" name="Name" value="@Model?.ProductName" />
	</div>

	<button type="submit" class="btn btn-primary mt-2">Submit</button>
</form>

<button form="htmlform" asp-action="submitform" class="btn btn-primary mt-2">
	Submit2
</button>

form요소에 추가된 id속성의 값은 form attribute의 값으로 button에서 사용되었으며 biutton이 click 되면 form이 Submit 될 것입니다. 상기 표에서 설명된 attribute들은 form의 대상을 식별하기 위해 사용되며 tag helper는 view rendering 될 때 URL을 생성하기 위해 routing system을 사용합니다. 아래 예제는 같은 기법을 Pages folder의 FormHandler.cshtml인 Razor Page에 적용한 것입니다.

<div class="m-2">
	<h5 class="bg-primary text-white text-center p-2">HTML Form</h5>
	<form asp-page="FormHandler" method="post" id="htmlform">
		<div class="form-group">
			<label>Name</label>
			<input class="form-control" name="Name" value="@Model.Products?.ProductName" />
		</div>
		<button type="submit" class="btn btn-primary mt-2">Submit</button>
	</form>
	<button form="htmlform" asp-page="FormHandler" class="btn btn-primary mt-2">
		Submit2
	</button>
</div>

project를 실행하고 /controllers/form 혹은 /pages/form으로 URL을 요청합니다. 그리고 응답으로 돌아온 HTML을 살펴보면 다음과 같이 form외부의 button이 변환되었음을 확인 할 수 있습니다.

<button form="htmlform" class="btn btn-primary mt-2" formaction="/controllers/Form/submitform">
	Submit2
</button>

새롭게 추가한 button은 form요소 내부의 submit과 동일하게 동작합니다.

4. input 요소 사용하기

 

input 요소는 HTML Form의 가장 근본적인 요소라고 할 수 있으며 사용자가 application에 구조화 되지 않은 data를 제공하기 위한 주요 수단입니다. InputTagHelper class는 input요소를 변환하는 데 사용되어 view model 속성의 data type과 형식을 반영합니다. 아래 표에서는 InputTagHelper에서 적용가능한 attribute를 나타내고 있습니다.

asp-for 해당 attribute는 input요소가 표현하는 view model 속성을 지정하는데 사용됩니다.
asp-format 해당 attribute는 view model 속성의 값을 위해 input 요소에서 사용되는 형식을 지정합니다.

asp-for attribute에서는 input요소의 name, id, type, value attribute를 설정하기 위해 사용되는 view model 속성의 이름을 설정합니다. 아래 예제는 Views/Form folder의 Form.cshtml file을 변경하여 input 요소가 asp-for attribute를 사용하도록 하였습니다.

<div class="form-group">
	<label>Name</label>
	<input class="form-control" asp-for="ProductName" />
</div>

위 예제의 tag helper는 model 표현식을 사용하여 asp-for attribute의 값을 이전과 달리 @문자를 사용하지 않고 지정하였습니다. project를 실행하여 /controllers/form URL을 요청하고 돌아오는 HTML응답을 살펴보면 tag helper가 input요소를 다음과 같이 변경하였음을 확인할 수 있습니다.

<input class="form-control" type="text" id="ProductName" name="ProductName" value="Chai">

id와 name attribute의 값은 model 표현식을 통해 가져온 것이며 form을 생성할때 오타가 발생되는 경우를 크게 줄여줄 수 있습니다. 다른 attribute들은 조금 더 복잡한데 특히 validation 하는 것과 많이 연관되어 있습니다.

Razor Page에서 Model 속성 선택
방금 알아본 asp-for attribute와 그 외 다른 tag helper들은 Razor Page에서도 사용될 수 있습니다. 하지만 변환된 요소에서 name과 id attribute의 값은 page model 속성의 name을 포함합니다. 예를 들어 아래 요소는 page model의 Products속성을 통해 Name속성을 선택하고 있습니다.
<input class="form-control" asp-for="Products.Name" />
그리고 뱐환된 요소에서는 id와 name속성을 다음과 같이 지정하게 됩니다.
<input class="form-control" type="text" id="Products_Name" name="Products.Name" />
이러한 차이는 data에서 form을 전달받기 위한 model binding 기능을 사용할 때  중요하다고 볼 수 있습니다.

 

(1) input 요소의 type attribute 변환

 

input요소의 type attribute는 browser가 요소를 어떤 형태로 표시하고 사용자가 입력한 값을 제한할지를 결정합니다. 위 예제의 input요소에서 type은 해당 요소에서 기본 type에 해당하는 text로 설정되었으며 어떠한 입력도 제한하지 않습니다. 아래예제는 Views/Form folder에 있는 Form.cshtml file을 변경한 것으로 type attribute가 처리되는 방식을 보여주기 위해  input요소를 form에 추가하였습니다.

<form asp-action="submitform" method="post" id="htmlform">
	<div class="form-group">
		<label>Id</label>
		<input class="form-control" asp-for="ProductId" />
	</div>

	<div class="form-group">
		<label>Name</label>
		<input class="form-control" asp-for="ProductName" />
	</div>

	<button type="submit" class="btn btn-primary mt-2">Submit</button>
</form>

예제에서 새롭게 추가된 input요소는 model의 ProductId속성을 추가하기 위해 asp-for attribute를 사용하였습니다. project를 실행하여 /controllers/form URL을 요청하고 응답되는 HTML을 확인해 보면 tag helper는 다음과 같은 요소로 변환되었음을 알 수 있습니다.

<input class="form-control" type="number" data-val="true" data-val-required="The ProductId field is required." id="ProductId" name="ProductId" value="1">

type attribute의 값은 asp-for attribute에서 지정된 view model의 속성의 유형에 의해 결정되는데 ProductId의 유형은 C#의 int형이므로 tag helper는 input요소의 type attribute를 number로 설정하게 됩니다. 또한 type이 number가 되면 input요소는 숫자값만 받아들일 수 있도록 제한됩니다. 그리고 위 응답을 살펴보면 요소에 data-val과 data-val-required attribute가 추가되어 있음을 알 수 있는데 이 attribute는 input요소의 validation을 돕는 역할을 할 수 있습니다. 아래 표에서는 input요소의 type attribute를 설정하기 위해 사용되는 C# type 간의 차이를 설명하고 있습니다.

browser가 type attribute의 값을 해석하는데는 특별한 제한이 없습니다. 이 말은 모든 browser가 HTML명세에 정의된 모든 type값에 대응하지 않는다는 뜻이 될 수 있으며 실제 이들이 구현되는 방식에서도 차이가 있습니다. type attribute는 form에서 예상할 수 있는 data에 대한 힌트가 될 수 있지만 사용자가 실제 이용가능한 data를 제공할 수 있도록 model validation기능을 사용하는 것이 좋습니다.
C# type input요소의 type
byte, sbyte, int, uint, short, ushort, long, ulong number
float, double, decimal text (model validation을 위한 추가 속성이 있는)
bool checkbox
string text
DateTime datetime

float, double, 그리고 decimal type은 input요소의 type을 text로 생성합니다. 왜냐하면 아직까지는 모든 browser가 해당 type에 맞는 값을 입력할 수 있도록 하기 위한 완전한 처리를 수행하지 않기 때문입니다. 따라서 tag helper는 input요소에 validation기능을 사용하기 위한 attribute를 추가하게 됩니다.

 

또한 명시적으로 input요소에서 type attribute 를 정의함으로써 위 표에서 나타난 기본적인 mapping을 재정의할 수도 있습니다. 이때 tag helper는 사용자가 정의한 type attribute값을 재정의하지 않습니다. 이러한 접근법의 단점은 model 속성을 통해 생성된 input요소가 존재하는 모든 view에서 type attribute를 설정해야 한다는 것인데 이렇게 하기보다는 C# model class의 속성에서 이래 표에 제시된 attribute 중 하나를 적용하는 편이 훨씬 유용할 것입니다.

tag helper는 model의 속성이 위 표에 해당되지 않고 속셩에 attribute가 적용되지 않은 경우라면 input요소의 type attribute에 text값을 설정합니다.

Attribute input요소의 type
[HiddenInput] hidden
[Text] text
[Phone] tel
[Url] url
[EmailAddress] email
[DataType(DataType.Password)] password
[DataType(DataType.Time)] type
[DataType(DataType.Date)] date

(2) input요소 값의 형식화

 

action method가 view model개체와 함께 view를 제공할때 tag helper는 input요소의 value attribute값을 설정하기 위해 asp-for attribute로 해당 model의 속성값을 사용하게 됩니다. 이때 asp-format attribute는 해당 data가 어떤 형식인지를 지정하는 데 사용됩니다. 아래 예제는 Views/Form folder에 있는 Form.cshtml file에 새로운 input요소를 추가하였는데 별도의 형식을 지정하지 않은 기본형식을 사용하도록 하고 있습니다.

<div class="form-group">
	<label>Name</label>
	<input class="form-control" asp-for="ProductName" />
</div>

<div class="form-group">
	<label>Price</label>
	<input class="form-control" asp-for="UnitPrice" />
</div>

project를 실행하고 /controllers/form/index/3 URL을 요청합니다. 그리고 되돌아 오는 HTML응답을 확인해 보면 다음과 같이 요소가 변환되어 있음을 알 수 있습니다. 기본적으로 input요소의 값은 model 속성의 값을 사용해 설정됩니다.

<input class="form-control" type="text" data-val="true" data-val-number="The field UnitPrice must be a number." id="UnitPrice" name="UnitPrice" value="10.00">

2개의 소수점을 가진 해당 형식은 database가 값이 저장되는 형식이기도 합니다.

자! 이제 ProductId가 3번인 UnitPrice의 값을 Database에서 아래와 같이 임의로 변경하고 난 후

Update [dbo].[Products]
Set UnitPrice = 99999.87
Where ProductId = 3;

asp-format attribute의 값을 지정하는데 이때 중괄호({})안에서 0:참조를 통해 표현형식을 지정합니다.

<div class="form-group">
	<label>Price</label>
	<input class="form-control" asp-for="UnitPrice" asp-format="{0:#,###.00}" />
</div>

project를 실행하고 이전과 같은 URL을 요청한뒤 응답 HTML을 확인해 보면 이번에는 요소가 다음과 같이 변환되어 있음을 알 수 있습니다.

<input class="form-control" type="text" data-val="true" data-val-number="The field UnitPrice must be a number." id="UnitPrice" name="UnitPrice" value="99,999.87">

당연한 이야기지만 asp-format attribute를 사용하기 위해서는 주어지는 값이 해당 표현형식을 지원하는 값이어야 하며 실제 사용 시에도 값을 통해 표현가능한 format만을 지정해야 합니다.

 

● Model Class를 통한 형식화 적용

 

만약 특정한 model 속성에서 항상 같은 표현형식이 사용되어야 한다면 C# model class에 System.ComponentModel.DataAnnotations namespace에 정의되어 있는 DisplayFormat attribute를 적용할 수 있습니다.

 

DisplayFormat attribute는 data값의 표현형식을 설정하기 위해 2개의 인수를 필요로 하는데 그중 DataFormatString은 문자열 표현형식을 지정하는 인수이며 ApplyFormatInEditMode는 input요소를 포함해 data를 입력/수정할 수 있는 요소에 값이 적용될 때 해당 표현형 식이 사용되어야 함을 지정하는 인수입니다. 아래 예제는 Model folder의 Products.cs file에서 UnitPrice속성에 이 attribute를 적용한 것으로 이전 예제와는 다른 형식을 지정하였습니다.

#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

namespace MyWebApp.Models
{
    public partial class Products
    {
        public Products()
        {
            OrderDetails = new HashSet<OrderDetails>();
        }

        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public int? SupplierId { get; set; }
        public int? CategoryId { get; set; }
        public string QuantityPerUnit { get; set; }

		[DisplayFormat(DataFormatString = "{0:c2}", ApplyFormatInEditMode = true)]
		public decimal? UnitPrice { get; set; }
        public short? UnitsInStock { get; set; }
        public short? UnitsOnOrder { get; set; }
        public short? ReorderLevel { get; set; }
        public bool Discontinued { get; set; }

        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
        public virtual Categories Category { get; set; }
        public virtual Suppliers Supplier { get; set; }
        public virtual ICollection<OrderDetails> OrderDetails { get; set; }
    }
}

asp-format attribute는 DisplayFormat attribute보다 더 우선권을 가지므로 아래와 같이 view에서 해당 attribute를 제거해야 합니다.

<input class="form-control" asp-for="UnitPrice" />

project를 실행하고 /controllers/form/index/3 URL을 요청하여 다음과 같은 응답이 생성되는지 확인합니다.

다시 말하자면 표현형식을 지정할때는 주의를 기울여야 하는데, 반드시 application에서 model binding과 validation기능을 을 사용해 표현된 값의 처리가 가능한 경우인지를 먼저 확인해야 합니다.

 

(3) input요소에서 관계data의 값을 표현하기

 

Entity Framework Core를 사용하는 것처럼 Database를 대상으로 하는 경우 대부분 관계 data로부터 가져온 값을 표시해야 하는 경우가 있습니다. 이때 asp-for attribute를 사용하면 중첩된 탐색 속성을 사용할 수 있으므로 수월하게 필요한 동작을 구현할 수 있습니다. 우선 아래 예제에서는 Controller folder의 FormController.cs file에서 view로 제공할 view model 개체에 관계 data를 포함하도록 하였습니다.

public async Task<IActionResult> Index(int id = 1)
{
	return View("Form", await context.Products.Include(p => p.Category).Include(p => p.Supplier).FirstAsync(p => p.ProductId == id));
}

이때 관계data에서는 view model개체를 serialize 하는 것이 아니므로 순환참조에 대한 걱정을 할 필요가 없습니다. 순환참조문제는 단지 web service coantroller에서만 중요할 뿐입니다. 아래 예제는 Views/Form folder의 Form.cshtml을 변경하여 관계 data를 선택하기 위한 asp-for attribute가 사용된 input요소를 추가하였습니다.

<div class="form-group">
	<label>Price</label>
	<input class="form-control" asp-for="UnitPrice" />
</div>

<div class="form-group">
	<label>Category</label>
	@{
#pragma warning disable CS8602
	}
	<input class="form-control" asp-for="Category.CategoryName" />
	@{
#pragma warning restore CS8602
	}
</div>

<div class="form-group">
	<label>Supplier</label>
	@{
#pragma warning disable CS8602
	}
	<input class="form-control" asp-for="Supplier.CompanyName" />
	@{
#pragma warning restore CS8602
	}
</div>

asp-for attribute의 값은 view model 개체와의 관계로 표현되며 Entity Framework Core가 Category와 Supplier Navigation속성에 할당한 관련 개체의 Name속성을 선택하도록 하는 중첩된 속성을 포함할 수 있습니다.

 

또한 null 조건 연산자는 model 표현식에서는 사용될 없으므로 Product.Category나 Product.Supplier속성처럼 null가능한 관계data의 속성을 선택하는 경우 문제가 될 수 있다는 경고가 표시될 수 있습니다. 이런 경우 속성의 type을 null이 될 수 없는 것으로 바꿈으로써 이러한 문제를 해결할 수 있지만 null가능한 속성이 특정 조건을 나타내기 위해 사용되는 경우처럼 몇몇 상황에서는 그렇게 하는 것도 불가능할 수 있습니다.

 

따라서 위 예제에서는 #pragma warning표현식을 사용해 null가능한 값이 안전하게 접근되지 않은 경우 경고를 생성하게 되는 warning CS8602에 대한 code분석을 차단하였습니다. tag helper는 asp-for attribute를 처리할때 null값에 대한 처리도 가능하므로 null에 대한 잠재적인 문제도 표시하지 않게 됩니다.

또한 compiler가 생성한 경고문제를 간단히 무시할 수도 있지만 가급적 해당 문제를 직접 해결하거나 해당 경고가 눈에 띄도록 하기 보다는 명시적으로 경고생성을 차단하는 것이 좋습니다.

같은 방법은 Page model 개체에 상대적으로 표현된 속성을 제외하고 Razor Page에도 그대로 사용될 수 있습니다. 아럐 예제는 Pages folder의 FormHandler.cshtml file에서 같은 방법이 적용된 것입니다.

<form asp-page="FormHandler" method="post" id="htmlform">
	<div class="form-group">
		<label>Name</label>
		<input class="form-control" asp-for="Products.ProductName" />
	</div>
	<div class="form-group">
		<label>Price</label>
		<input class="form-control" asp-for="Products.UnitPrice" />
	</div>
	<div class="form-group">
		<label>Category</label>
				@{
	#pragma warning disable CS8602
				}
		<input class="form-control" asp-for="Products.Category.CategoryName" />
				@{
	#pragma warning restore CS8602
				}
		</div>
		<div class="form-group">
		<label>Supplier</label>
				@{
	#pragma warning disable CS8602
				}
		<input class="form-control" asp-for="Products.Supplier.CompanyName" />
				@{
	#pragma warning restore CS8602
				}
		</div>
	<button type="submit" class="btn btn-primary mt-2">Submit</button>
</form>
public async Task OnGetAsync(int id = 1)
{
	Products = await context.Products.Include(p => p.Category).Include(p => p.Supplier).FirstAsync(p => p.ProductId == id);
}

project를 실행하고 /controllers/form으로 URL을 요청하여 다음과 같은 결과를 확인합니다.

물론 동일한 구현을 Page에도 적용하였으므로 /pages/form으로 URL을 요청해도 비슷한 결과를 볼 수 있습니다.

 

5. label 요소 사용하기

 

LabelTagHelper class는 label요소를 변환하는데 사용되며 for attribute를 input요소를 변환할 때 사용했던 접근법과 동일한 방법으로 설정할 수 있습니다. 아래표는 해당 tag helper가 지원하는 attribute를 나열한 것입니다.

asp-for 이 attribute는 label요소에서 사용될 view model 속성을 지정합니다.

해당 tag helper는 label 요소의 content를 설정하는데 view model 속성에서 선택된 이름을 포함하게 됩니다. 또한 for attribute를 사용하여 특정 input요소와의 연결성을 표시할 수도 있습니다. 이렇게 해서 화면 판독기에 의존하는 사용자나  label을 click 함으로써 연결된 input요소에 초점을 맞출 수 있도록 돕게 됩니다.

 

아래 예제는 Views/Form folder의 Form.cshtml file을 변경하여 Form view에 asp-for attribute를 적용함으로서 각 label요소가 같은 view model 속성을 표현하는 input요소 연결되도록 하였습니다.

<form asp-action="submitform" method="post" id="htmlform">
	<div class="form-group">
		<label asp-for="ProductId"></label>
		<input class="form-control" asp-for="ProductId" />
	</div>

	<div class="form-group">
		<label asp-for="ProductName"></label>
		<input class="form-control" asp-for="ProductName" />
	</div>

	<div class="form-group">
		<label asp-for="UnitPrice"></label>
		<input class="form-control" asp-for="UnitPrice" />
	</div>

	<div class="form-group">
		<label asp-for="Category.CategoryName"></label>
		@{
	#pragma warning disable CS8602
		}
		<input class="form-control" asp-for="Category.CategoryName" />
		@{
	#pragma warning restore CS8602
		}
	</div>

	<div class="form-group">
		<label asp-for="Supplier.CompanyName"></label>
		@{
	#pragma warning disable CS8602
		}
		<input class="form-control" asp-for="Supplier.CompanyName" />
		@{
	#pragma warning restore CS8602
		}
	</div>

	<button type="submit" class="btn btn-primary mt-2">Submit</button>
</form>

또한 예제에서 마지막 2개의 label처럼 label의 content는 직접 재정의할 수 있습니다. 특히 속성의 Name을 content로 설정하였는데 이보다는 사용자가 좀 더 유익한 content를 적용할 수도 있을 것입니다. project를 실행하여 /controllers/form URL을 호출하고 다음과 같은 결과를 확인합니다.

6. Select와 Option요소 사용하기

 

select과 option은 사용자에게 input요소에서 처럼 개방형 data 입력보다는 고정된 값의 선택을 제공하기위해 사용됩니다. SelectTagHelper class는 select요소를 변환하며 아래 표의 attribute를 지원합니다.

asp-for 이 attribute는 select요소가 표현할 view또는 page model 속성을 선택하기 위해 사용됩니다.
asp-items 이 attribute는 select요소 안에 포함되는 option요소에 대한 값의 source를 지정하는데 사용됩니다.

asp-for에는 model속성을 반영하기 위해 for와 id attribute의 값을 설정합니다. 아래 예제는 Views/Form folder의 Form.cshtml file을 변경하여 Category의 input요소를 값의 고정된 범위를 사용자에게 제공하는 select요소로 바꾼 것입니다.

<div class="form-group">
	<label asp-for="Category.CategoryName"></label>
	<select class="form-control" asp-for="CategoryId">
		<option value="1">Watersports</option>
		<option value="2">Soccer</option>
		<option value="3">Chess</option>
	</select>
</div>

예제에서는 특정한 값으로만 option요소를 통해 select요소를 추가하여 사용자로 하여금 특정한 범위의 Cateogry만을 선택할 수 있도록 하였습니다. project를 실행하여 /controllers/form/index/3 URL을 요청하고 되돌아오는 HTML응답을 확인해 보면 tag helper가 select요소를 다음과 같이 변환하였음을 알 수 있습니다.

<select class="form-control" id="CategoryId" name="CategoryId">
		<option value="1">Watersports</option>
		<option value="2" selected="selected">Soccer</option>
		<option value="3">Chess</option>
</select>

HTML에서 selected attribute가 option요소에 추가되었으며 view model의 CateogryId값에 해당합니다.

 

option요소를 선택하는 처리는 SelectTagHelper로부터 TagHelperContext.Items collection을 전달받아 OptionTagHelper class에 의해 수행됩니다. 그런 뒤 Product개체의 CategoryId값과 관련된 Category의 이름을 최종적으로 표시하게 됩니다.

 

(1) select 요소 채우기

 

select요소를 위해 명시적으로 option요소를 정의하는 것은 항상 같은 값을 선택하는 경우에는 괜찮은 접근법이지만 data model로부터 가져온 값을 제공하거나 여러 뷰에서 동일한 option을 설정해야 할 때 content를 중복적으로 관리하고 싶지 않은 경우라면 별로 도움이 되지 않을 것입니다.

 

이런 경우 asp-items attribute는 option요소를 생성할 SelectListItem개체의 list 배열로 tag helper를 제공하기 위해 사용됩니다. 아래 예제는 Controllers folder의 FormController.cs file을 변경하여 Form controller의 Index action으로 view bag을 통해 SelectListItem개체의 배열을 view와 함께 제공하도록 하였습니다.

public async Task<IActionResult> Index(int id = 1)
{
	ViewBag.Categories = new SelectList(context.Categories, "CategoryId", "CategoryName");

	return View("Form", await context.Products.Include(p => p.Category).Include(p => p.Supplier).FirstAsync(p => p.ProductId == id));
}

SelectListItem 개체는 직접적으로 생성될 수 있지만 ASP.NET Core는 이미 존재하는 data의 배열을 위해 SelectList class를 제공하고 있습니다. 위 예제의 경우 database로부터 가져온 Category개체의 배열을 option요소의 값과 label로 사용될 속성의 이름과 함께 SelectList생성자로 전달하고 있습니다. 아래 예제는 Views/Form folder의 Form.cshtml file을 변경하여 SelectList를 사용하기 위해 Form view를 변경하였습니다.

<div class="form-group">
	<label asp-for="Category.CategoryName"></label>
	<select class="form-control" asp-for="CategoryId" asp-items="@ViewBag.Categories"></select>
</div>

project를 실행하고 /controllers/form/index/3으로 URL을 요청합니다. 사용자에게 보이는 content상으로는 변화된 게 없지만 select요소를 채우는 데 사용된 option요소가 database로부터 다음과 같이 생성되었습니다.

<select class="form-control" id="CategoryId" name="CategoryId"><option value="1">Beverages</option>
<option selected="selected" value="2">Condiments</option>
<option value="3">Confections</option>
<option value="4">Dairy Products</option>
<option value="5">Grains/Cereals</option>
<option value="6">Meat/Poultry</option>
<option value="7">Produce</option>
<option value="8">Seafood</option>
</select>

이러한 접근법은 사용자에게 표현되는 option요소가 자동적으로 database에 추가된 새로운 Category를 반영할 수 있게 합니다.

 

7. Text Area 사용하기

 

textarea는 사용자로부터 많은 양의 text(일반적으로 공지나 의견과 같은 구조회 되지 않은 data) 입력을 요청하기 위해 사용됩니다. TextAreaTagHelper class는 이러한 textarea요소를 변환하는 데 사용되며 아래표의 attribute를 지원합니다.

asp-for 이 attribute는 textarea요소에서 표현할 view model 속성을 지정하는데 사용됩니다.

TextAreaTagHelper는 비교적 단순하며 asp-for attribute에 제공되는 값은 textarea요소의 id와 name attribute를 설정하는 데 사용되고 asp-for attribute에서 지정된 속성의 값은 textarea요소의 content로서 사용됩니다. 아래 예제에서는 Views/Form folder의 Form.cshtml file에서 Supplier.CompanyName속성에 사용된 input요소를 asp-for attribute가 적용된 textarea요소로 변경되도록 수정한 것입니다.

<div class="form-group">
	<label asp-for="Supplier.CompanyName"></label>
	@{
#pragma warning disable CS8602
	}
	<textarea class="form-control" asp-for="Supplier.CompanyName"></textarea>
	@{
#pragma warning restore CS8602
	}
</div>

project를 실행한 뒤 /controllers/form으로 URL을 요청합니다. 그리고 되돌아오는 HTML응답을 확인해 보면 다음과 같이 textarea요소로 변환되었음을 알 수 있습니다.

<textarea class="form-control" id="Supplier_CompanyName" name="Supplier.CompanyName">Exotic Liquids</textarea>

8. Anti-forgery기능 사용하기

 

가장 처음예제에서 form data를 처리하는 controller action method와 page handler method를 정의할 때 이름이 _(밑줄)로 시작하는 form data를 filtering 하였습니다.

public IActionResult SubmitForm()
{
    foreach (string key in Request.Form.Keys.Where(k => !k.StartsWith("_")))
    {
        TempData[key] = string.Join(", ", Request.Form[key]);
    }

    return RedirectToAction(nameof(Results));
}

위와 같은 filter를 적용한 이유는 form에서 HTML에 의해 제공된 값에 초점을 맞추기 위해서였습니다. 이제 아래와 같이 해당 filter를 삭제하여 HTML form으로부터의 모든 data가 temp data에 저장되도록 합니다.

foreach (string key in Request.Form.Keys)
{
    TempData[key] = string.Join(", ", Request.Form[key]);
}

project를 실행하고 /controllers/form URL을 요청한 뒤 submit button을 눌러 form을 application으로 전송합니다. 그러면 다음과 같이 새로운 항목이 표시되는 것을 볼 수 있습니다.

form값으로 표시된 _RequestVerificationToken은 보안과 관련된 것 중 하나로 FormTagHelper에 의해 cross-site 요청위조를 방지하고자 적용된 것입니다. CSRF(Cross-site request forgery)는 일반적으로 사용자의 요청이 인증되는 방식을 이용하여 웹 application을 악용하는 것을 말합니다. ASP.NET Core를 사용하여 생성된 것을 포함해 대부분의 Web Application은 cookie를 사용해 일반적으로 사용자 ID와 연결되어 있는 특정 session과 관련된 요청을 식별합니다.

 

XSRF로도 알려진 CSRF는 또한 자신의 session을 명시적으로 종료하지 않고 다른 web application을 사용한 이후에 악성 web site를 방문하는 사용자를 목표로 합니다. 이때 application은 여전히 활동적인 사용자 session과 관련되어 있으며 browser가 저장한 cookie가 아직 만료되지 않은 경우라면 악성 web site는 application으로 form요청을 보내는 javascript code를 사용하여 사용자의 동의 없이 악의적인 동작을 수행하게 됩니다. 이때 동작에 대한 정확한 특성은 공격이 시작되는 application에 따라 달라지지만 일단 사용자의 browser에 의해 javascript code가 실행되고 나면 application에 대한 요청에는 session cookie가 포함되고 application은 사용자의 동의없이 원하는 동작을 수행합니다.

 

CSRF에 관한 자세한 내용은 아래 link를 참고하시기 바랍니다.

Cross-site request forgery - Wikipedia

 

Cross-site request forgery - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Malicious website exploit where unauthorized commands are transmitted from a trusted user Cross-site request forgery, also known as one-click attack or session riding and abbreviated a

en.wikipedia.org

form요소가 action attribute를 포함하고 있지 않다면(asp-controller, asp-action, asp-page attribute를 통한 routing system으로부터 생성되었으므로) FormTagHelper class는 자동적으로 anti-CSRF기능을 사용하도록 설정하여 보안 token이 응답에 cookie로 추가될 것입니다. 이를 위해 같은 보안 token을 가진 숨겨진 input요소는 위에서 처럼 HTML form에 추가됩니다.

 

(1) Controller에서 Anti-forgery기능 사용하기

 

기본적으로 controller에서는 필요한 보안 token을 포함하고 있지 않는다 하더라도 POST요청을 받을 수 있습니다. 이때 anti-forgery기능을 사용하려면 Controllers folder에 있는 FormController.cs에서와 같이 controller class에 아래와 같은 attribute를 적용해야 합니다.

[AutoValidateAntiforgeryToken]
public class FormController : Controller

모든 요청이 anti-forgery token을 필요로 하는 것은 아니므로 AutoValidateAntiforgeryToken은 GET, HEAD, OPTIONS, TRACE를 제외한 다른 모든 HTTP요청에서 Validation을 수행하도록 합니다.

다른 2개의 attribute는 token validation을 제어하기 위해 사용될 수 있습니다. 우선 IgnoreValidationToken attribute는 action method나 controller에서 validation을 수행하지 않도록 하며 ValidateAntiForgeryToken attribute는 반대로 validation을 수행하도록 하는 것인데 HTTP GET요청과 같은 일반적으로 validation을 필요로 하지 않는 요청에 대해서도 validation이 적용됩니다. 이 2개의 attribute는 매우 극단적으로 작동하는 경우라서 되도록 이면 AutoValidateAntiforgeryToken을 사용할 것을 권장합니다.

anti-CSRF기능을 확인하는 것은 조금 까다로울 수 있는데 우선 project를 실행하여 /controllers/forms로 URL을 요청합니다. 그리고 browser의 F12 Key를 눌러 개발자도구를 열어준 다음 숨겨져 있는 input요소를 삭제해 줍니다.

그리고 form의 submit을 click 하게 되면 요청을 실패하게 될 것입니다.

 

(2) Razor Page에서 Anti-forgery기능 사용하기

 

Razor Page에서 Anti-forgery기능은 기본적으로 사용됩니다. 이 것은 FormHandler page를 생성할 때의 위 예제에서 page handler method에 IgnoreAntiforgeryToken attribute를 적용하게 된 이유이기도 합니다. 아래 예제는 Pages folder의 FormHandler.cshtml file에서 적용했던 IgnoreAntiforgeryToken을 주석처리하여 Request Validation을 사용하도록 한 것입니다.

//[IgnoreAntiforgeryToken]
public class FormHandlerModel : PageModel

validation feature기능을 확인해 보려면 위 controller에서 했던 것과 같은 방법을 사용합니다. F12 key로 개발자 도구를 열고 form을 application으로 submit 하기 전에 해당 보안 key를 가지고 있는 요소를 삭제합니다.

 

(3) JavaScript Client에서 Anti-forgery Token사용하기

 

기본적으로 ASP.NET Core application에서 사용하는 anti-forgery기능은 form이 submit 될 때 browser가 돌려주는 HTML에 요소로 포함될 수 있습니다. 그런데 이건 javascript client에서는 작동할 수 없는데 ASP.NET Core Application은 data를 제공해 주는 것이지 HTML을 제공하는 게 아니기 때문입니다. 결국 hidden요소를 추가하고 향후 요청에서 이를 받을 수 있는 방법은 없습니다.

 

web service에서 anti-forgery token은 javascript가 읽을 수 있는 cookie로서 전송될 수 있으며 javascript client code가 읽고 POST요청에서 이를 header로 포함시킬 수 있습니다. Angular와 같은 몇몇 javascript framework는 자동적으로 cookie를 감지하고 자동적으로 요청에서 header로 포함시키기도 하지만 다른 framework나 사용자 정의 javascript code에서는 추가적인 작업이 필요할 수 있습니다.

 

아래 예제는 ASP.NET Core application이 JavaScript client사용할 anti-forgery feature기능을 구성하기 위 필요한 변경사항을 나타내고 있습니다.

using Microsoft.EntityFrameworkCore;
using MyWebApp.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Razor.TagHelpers;
using MyWebApp.TagHelpers;
using Microsoft.AspNetCore.Antiforgery; //변경사항

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<NorthwindContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:DefaultConnection"]);
    opts.EnableSensitiveDataLogging(true);
});

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddSingleton<CitiesData>();
//builder.Services.AddTransient<ITagHelperComponent, TimeTagHelperComponent>();
//builder.Services.AddTransient<ITagHelperComponent, TableFooterTagHelperComponent>();

//변경사항
builder.Services.Configure<AntiforgeryOptions>(opts => {
	opts.HeaderName = "X-XSRF-TOKEN";
});

var app = builder.Build();
app.UseStaticFiles();

//변경사항
IAntiforgery antiforgery = app.Services.GetRequiredService<IAntiforgery>();
app.Use(async (context, next) => {
	if (!context.Request.Path.StartsWithSegments("/api"))
	{
		string? token = antiforgery.GetAndStoreTokens(context).RequestToken;

		if (token != null)
			context.Response.Cookies.Append("XSRF-TOKEN", token, new CookieOptions { HttpOnly = false });
	}

	await next();
});

//app.MapControllers();
//app.MapDefaultControllerRoute();
app.MapControllerRoute("forms", "controllers/{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

option pattern은 AntiforgeryOptions class를 통해 anti-forgery기능을 구성하기 위해 사용되었습니다. HeaderName속성은 또한 anti-forgery token이 허용될 header의 이름을 지정하는 데 사용되었으며 이 경우 이름은 'X-XSRF-TOKEN'이 됩니다.

 

사용자 정의 middleware component는 예제에서 이름이 'XSRF-TOKEN'인 cookie를 설정하는데 필요한데 cookie의 값은 IAntiForgery service를 통해 가져오게 되며 반드시 HttpOnly option이 false로 설정되어야 합니다. 그래야 browser는 javascript code가 cookie를 읽을 수 있도록 허용할 것이기 때문입니다.

예제에서는 Angular에서 지원되는 명칭을 사용하였습니다. 다른 framework의 경우 그들만의 규칙이 존재할 테지만 일반적으로 모든 cookie와 header이름 사용할 수 있도록 설정할 수 있습니다.

cookie와 header를 사용하는 JavaScript client를 만들기 위해 JavaScriptForm.cshtml이름의 file을 Pages folder에 아래와 같이 추가합니다.

@page "/pages/jsform"

<script type="text/javascript">
	async function sendRequest() {
		const token = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, "$1");

		let form = new FormData();
		form.append("name", "Paddle");
		form.append("price", 100);
		form.append("categoryId", 1);
		form.append("supplierId", 1);

		let response = await fetch("@Url.Page("FormHandler")", {
			method: "POST",
			headers: { "X-XSRF-TOKEN": token },
			body: form
		});

		document.getElementById("content").innerHTML = await response.text();
	}

	document.addEventListener("DOMContentLoaded", () => document.getElementById("submit").onclick = sendRequest);
</script>

<button class="btn btn-primary m-2" id="submit">Submit JavaScript Form</button>
<div id="content"></div>

예제의 Razor Page의 javascript code는 button click 되면 FormHandler Razor Page로 HTTP POST요청을 전송하게 됩니다. 이때 XSRF-TOKEN cookie의 값이 읽혀 X-XSRF-TOKEN 요청 header에 포함됩니다. 이후 FormHandler page로부터의 응답은 browser가 자동으로 따라오는 Results page로 redirection을 수행합니다. Results page로 부터의 응답은 JavaScript code에 의해 읽히고 요소에 추가되므로 사용자에게 표시될 수 있습니다. javascript code를 test 하기 위해 project를 실행하여 /pages/jsform으로 URL을 요청한 뒤 button을 click 합니다. javascript code는 form을 submit 하고 다음과 같은 응답을 표시할 것입니다.

728x90