.NET/ASP.NET

ASP.NET Core - 5. Platform 기능 활용하기 - 2

클리엘 2022. 11. 10. 14:19
728x90

이번 글에서는 지난 글에 이이서 ASP.NET Core platform에서 제공하는 기본적인 기능들에 대해 계속 설명을 이어나가고자 합니다. 우선 cookie에 관해 알아볼 텐데 어떻게 cookie가 사용되며 이를 위해 사용자의 동의가 어떠한 방법으로 관리될 수 있는지, 그리고 cookie의 강력한 대안인 session을 어떻게 다룰지에 대해서도 함께 알아볼 것입니다. 이어서 HTTPS에 대한 요청 처리와 HTTPS로의 요청 강제할 수 있는 방법, 그리고 error의 처리방법과 함께 Host header에 기반한 요청을 어떻게 filter 할 수 있을지에 대해서도 같이 알아보겠습니다.

 

아래 표는 위에서 언급한 주요 내용에 대해 간결한 특징을 나열한 것입니다.

cookie 사용 cookie를 일고 쓰기 위한 context사용
cookie 사용동의 관리 consent middleware사용
요청 전반에 걸친 data 저장 session 사용
HTTP 요청 보안처리 HTTPS miiddleware 사용
Error 제어 error와 status code middleware 사용
host header를 통한 요청 제한 configuration 설정에서 AllowedHosts 설정

1. Project 준비하기

 

예제로 사용할 Project는 이전 Project를 계속 이어서 사용할 것입니다. 다만 Program.cs에서는 이전에 생성한 Middleware와 Service 등을 삭제하고 전체 내용을 아래와 같이 바꿔줍니다.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapFallback(async context => await context.Response.WriteAsync("Hello World!"));

app.Run();

2. Cookie

 

Cookie는 응답에 추가된 작은 text로서 다양한 일련의 HTTP 요청을 WebBrowser가 Server로 보내는 Cookie에 의해 식별될 수 있으므로 Web Application에서는 매우 중요한 요소로 작용합니다. ASP.NET Core에서는 Middleware component로 제공되는 HttpRequest와 HttpResponse개체를 통해 Cookie를 사용할 수 있는데 이를 위한 하나의 예를 들어보기 위해 Program.cs에서 counter를 구현한 endpoint를 아래와 같이 추가하여 routing 설정을 바꿔줍니다.

using System.Net.NetworkInformation;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/cookie", async context => {
    int counter1 = int.Parse(context.Request.Cookies["counter1"] ?? "0") + 1;

    context.Response.Cookies.Append("counter1", counter1.ToString(), new CookieOptions { MaxAge = TimeSpan.FromMinutes(30) });
    
    int counter2 = int.Parse(context.Request.Cookies["counter2"] ?? "0") + 1;
    
    context.Response.Cookies.Append("counter2", counter2.ToString(), new CookieOptions { MaxAge = TimeSpan.FromMinutes(30) });
    
    await context.Response.WriteAsync($"Counter1: {counter1}, Counter2: {counter2}");
});

app.MapGet("clear", context => {
    context.Response.Cookies.Delete("counter1");
    context.Response.Cookies.Delete("counter2");
    context.Response.Redirect("/");

    return Task.CompletedTask;
});

app.Run();

새롭게 추가한 endpoint는 counter1과 counter2의 Cookie에 의존하고 있는데 /cookie로의 URL이 요청되면 middleware는 Cookie를 찾아 int형으로 값을 Parse 하게 됩니다. 다만 Cookie가 없다면 예제에서처럼 그에 대한 대비로 0 값이 사용됩니다.

 

Cookie는 Cookie의 이름이 Key로서 사용되는 곳에서 HttpRequest.Cookies속성을 통해 접근할 수 있습니다. 예제에서는 Cookie로부터 가져온 값을 증가시키고 해당 값을 다시 응답의 Cookie로 설정하는 데 사용하고 있습니다.

 

Cookie는 다시 HttpResponse.Cookies속성을 통해 설정될 수 있으며 Append() Method를 통해 응답으로의 Cookie를 생성하거나 교체할 수 있습니다. Append Method의 매개변수로는 Cookie의 이름과 값 그리고 Cookie자체를 설정하는 데 사용되는 CookieOptions 객체가 사용됩니다. CookieOptions class는 아래 표에 따른 속성을 정의하고 있는데 각각은 Cookie의 Field에 해당합니다.

<표1-1>
Domain 이 속성은 Browser가 Cookie를 보낼 Host를 특정합니다. 기본적으 Cookie는 Cookie가 생성된 Host로만 전송됩니다.
Expires 이 속성에서는 cookie의 만료를 지정합니다.
HttpOnly 이 속성이 true이면 Browser는 JavaScript에서 만들어진 요청에서는 Cookie를 포함하지 않음을 알려주게 됩니다.
IsEssential 이 속성은 Cookie가 필수인지를 나타내는데 사용됩니다.
MaxAge 이 속성은 Cookie가 만료될때 까지의 시간을 특정합니다. 단, 다소 오래된 Browser의 경우 이 설정을 통한 Cookie의 사용을 지원하지 않을 수 있습니다.
Path 이 속성은 Browser에서 Cookie를 보내기 전에 요청에 있어야 하는 URL path를 설정하는데 사용됩니다.
SameSite 이 속성은 Cookie가 cross-site 요청이 포함되어야 하는지의 여부를 결정하는데 사용됩니다.
Secure 이 속성이 true이면 Browser는 Cookie를 오로지 HTTPS를 사용하는 경우에만 보내야 합니다.

Cookie는 응답 Header를 통해 전송되므로 응답 본문이 작성되기 이전에 설정될 수 있고 그 후에 Cookie에 대한 모든 변경사항은 무시됩니다.

 

위 예제에서 Cookie option은 단지 MaxAge 만들 설정하고 있으며 이를 통해 Browser에게 Cookie가 30분 후에 만료될 것임을 알려주고 있습니다. 또한 Middleware는 /clear URL요청이 발생하는 경우 모든 Cookie를 삭제하도록 하고 있는데 예제에서는 이를 위해 HttpResponse.Cookie.Delete Method를 사용한 후 곧장 / URL로 Redirect를 수행하도록 하고 있습니다.

 

Project를 실행해 /cookie로 URL을 반복적으로 요청하여 아래와 같이 값이 증가된 형태의 결과가 발생하는지를 확인합니다. 아마도 Browser는 reload가 발생할 때마다 증가된 counter값을 표시할 것입니다.

그런 뒤 /clear URL로 요청을 보내면 이번에는 Cookie를 모두 삭제하므로 counter값은 다시 1로 초기화될 것입니다.

 

(1) Cookie 동의 확인

 

EU 일반 데이터 보호 규정:GDPR(The EU General Data Protection Regulation)에서는 불필요한 Cookie가 사용되기 전에  사용자의 동의가 있어야 한다는 것을 명시하고 있습니다. 이를 위해 ASP.NET Core에서는 사용자의 동의를 얻고 사용자의 동의 없이 불필요한 Cookie가 Web Browser로 전송되는 것을 방지하는 것을 지원하고 있습니다. 여기서 option pattern은 Middleware component에 의해 적용되는 정책을 생성하는 데 사용됩니다.

Cookie consent는 GDPR의 한 부분일 뿐입니다. 아래 Link에서 해당 규정의 개관을 확인하실 수 있습니다.

General Data Protection Regulation - Wikipedia

 

General Data Protection Regulation - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search European regulation on personal data The General Data Protection Regulation (EU) (GDPR) is a regulation in EU law on data protection and privacy in the European Union (EU) and the Euro

en.wikipedia.org

using System.Net.NetworkInformation;

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<CookiePolicyOptions>(opts => {
    opts.CheckConsentNeeded = context => true;
});

var app = builder.Build();

app.UseCookiePolicy();

app.MapGet("/cookie", async context => {
    int counter1 = int.Parse(context.Request.Cookies["counter1"] ?? "0") + 1;

    context.Response.Cookies.Append("counter1", counter1.ToString(), new CookieOptions { MaxAge = TimeSpan.FromMinutes(30), IsEssential = true });
    
    int counter2 = int.Parse(context.Request.Cookies["counter2"] ?? "0") + 1;
    
    context.Response.Cookies.Append("counter2", counter2.ToString(), new CookieOptions { MaxAge = TimeSpan.FromMinutes(30) });
    
    await context.Response.WriteAsync($"Counter1: {counter1}, Counter2: {counter2}");
});

app.MapGet("clear", context => {
    context.Response.Cookies.Delete("counter1");
    context.Response.Cookies.Delete("counter2");
    context.Response.Redirect("/");

    return Task.CompletedTask;
});

app.Run();

예제에서 option pattern은 CookiePolicyOptions객체를 설정하는 데 사용되었는데 CookiePolicyOptions을 통해서는 application에서의 Cookie정책에 대한 전반적인 설정을 아래 표의 Method를 사용해 적용할 수 있습니다.

CheckConsentNeeded 이 속성은 HttpContext객체를 수신하고 Cookie동의를 필요로 하는 요청이 표현되는 경우 true를 반환하는 function을 할당합니다. fuction은 모든 요청에 호출되는데 기본적으로는 항상 false를 반환합니다.
ConsentCookie 사용자의 Cookie동의를 기록하기 위해 WebBrowser로 Cookie를 전송하게 되는데, 이 속성은 해당 Cookie를 구성하는데 사용된는 객체를 반환합니다.
HttpOnly 이 속성은 <표1-1>에서 설명된 HttpOnly속성의 기본값을 설정합니다.
MinimumSameSitePolicy 이 속성은 <표1-1>에서 설명된 SameSite속성의 최저보안 수준을 설정합니다.
Secure 이 속성은 <표1-1>에서 설명된 Secure속성의 기본값을 설정합니다.

예제에서는 동의 확인을 사용하기 위해 true를 반환하는 새로운 function을 CheckConsentNeeded속성에 할당하였습니다. 이 function은 ASP.NET Core가 수신하는 모든 요청에 호출되는데 이러한 방법을 통해서 우리는 동의가 필요한 요청을 선택하기 위해 좀 더 정교한 규칙을 적용할 수 있습니다. 예제에서는 가장 조심스러운 접근법을 취했고 모든 요청에 동의를 구하고 있습니다.

 

Cookie정책을 강제하는 Middleware는 UseCookiePolicy Method를 통해 요청 Pipeline에 추가되었습니다. 이 결과 IsEssential속성이 true인 Cookie만이 응답에 추가될 것입니다. 위 예제에서는 IsEssential속성을 cookie1에만 설정하였고 그 결과를 Project를 실행하여 /cookie URL을 요청함으로써 확인할 수 있습니다.

Browser를 새로고침 하면 essential로서 표시된 Cookie의 counter만이 update 될 것입니다.

(2) Cookie 동의 관리하기

 

사용자가 동의를 하지 않는 한 Web Application이 동작하기 위한 필수 Cookie만이 허용됩니다. 동의는 요청 기능을 통해 관리되며 요청 기능은 ASP.NET Core가 요청과 응답을 처리하는 방법에 대한 세부적인 구현에 접근하는 middleware component를 제공합니다. 기능은 HttpRequest.Features속성을 통해 접근되며 각 요청은 저수준 요청 처리를 다루는 속성과 Method를 정의하는 Interface로 표현됩니다.

 

이 기능은 응답의 구조와 같이 거의 변경할 필요가 없는 요청을 처리하는 한 측면을 다루게 됩니다. 그러나 ITrackingConsentFeature Interface를 통해 처리하는 Cookie동의 관리는 예외인데 이것은 아래 속성과 Method를 정의하고 있습니다.

CanTrack 이 속성은 현재 요청에 불필요한 Cookie가 추가될때 true를 반환하는데 이것은 사용자가 동의를 했거나 동의가 필요하지 않는 경우입니다.
CreateConsentCookie() 이 Method는 JavaScript client에서 동의를 표시하기 위해 사용될 수 있는 cookie를 반환합니다.
GrantConsent() 이 Method는 비필수적인 Cookie를 위한 동의를 부여하는 응답에 Cookie를 추가합니다.
HasConsent 이 속성은 사용자가 비필수 Cookie에한 동의를 부여한 경우 true를 반환합니다.
IsConsentNeeded 이 속성은 현재 요청에 비필수 Cooke에 대한 동의가 필요한 경우 true를 반환합니다.
WithdrawConsent() 이 Method는 동의 Cookie를 삭제합니다.

실제 동의를 다루기 위해 ConsentMiddleware.cs라는 이름의 file을 Project의 Infrastructure에 아래 code로 추가합니다. Lambd 식을 통해 Cooke동의 관리를 수행할 수 있지만 설정 Method의 깔끔함을 유지하기 위해 class를 사용하였습니다.

using Microsoft.AspNetCore.Http.Features;

namespace MyWebApp.Infrastructure
{
    public class ConsentMiddleware
    {
        private RequestDelegate next;
        public ConsentMiddleware(RequestDelegate nextDelgate)
        {
            next = nextDelgate;
        }
        public async Task Invoke(HttpContext context)
        {
            if (context.Request.Path == "/consent")
            {
                ITrackingConsentFeature? consentFeature = context.Features.Get<ITrackingConsentFeature>();

                if (consentFeature != null)
                {
                    if (!consentFeature.HasConsent)
                        consentFeature.GrantConsent();
                    else
                        consentFeature.WithdrawConsent();

                    await context.Response.WriteAsync(consentFeature.HasConsent ? "Consent Granted \n" : "Consent Withdrawn\n");
                }
            }
            else
            {
                await next(context);
            }
        }
    }
}

요청 Method는 Get Method를 통해 가져올 수 있는데 여기에는 Generic type인수로 필요한 기능 Interface를 특정하고 있습니다.

ITrackingConsentFeature? consentFeature = context.Features.Get<ITrackingConsentFeature>();

새로운 Middleware component는 위에서 언급한 속성과 Method를 사용해 /consent URL로 Cookie에 대한 동의를 결정하거나 바꾸기 위한 응답을 수행하므로 아래와 같이 Middleware compoennt를 요청 Pipeline에 추가합니다.

var app = builder.Build();

app.UseCookiePolicy();
app.UseMiddleware<MyWebApp.Infrastructure.ConsentMiddleware>();

Project를 실행하여 /consent URL을 요청합니다.

'consent granted'가 표시되었다면 바필수 Cookie가 허용되었다는 것을 의미하는 것이며 /cookie URL를 다시 요청한 뒤 Browser를 새로고침 하면  2개의 counter모두 값이 증가하는 것을 볼 수 있습니다.

이제 다시 /consent로 URL을 요청합니다. 이번에는 withdraw이 표시될 것입니다.

이 상태에서 /cookie URL을 요청하면 이번에는 counter1만이 값이 증가하는 것을 볼 수 있습니다.

왜냐하면 counter1의 Cookie는 필수 Cookie로 지정되어 있기 때문입니다.

context.Response.Cookies.Append("counter1", counter1.ToString(), new CookieOptions { MaxAge = TimeSpan.FromMinutes(30), IsEssential = true });

3. Session

 

이전 예제에서는 Middleware component에 필요한 data를 제공하는 Cookie를 사용하여 Application의 상태를 저장하였습니다. 그런데 Cookie는 Client에 저장된다는 특징이 있습니다. 이러한 특징 때문에 data는 조작될 수 있고 Application의 동작을 변경시킬 수 있다는 단점이 발생합니다.

 

이러한 단점을 극복하기 위한 또 다른 방법으로 ASP.NET Core에서는 Session을 사용할 수 있습니다. Session Middleware는 Cookie를 응답에 추가하여 관련 요청이 식별될 수 있도록 하며 Server에 저장된 data와도 연결됩니다.

 

Session cookie가 포함된 요청이 도달하면 Seesion Middleware Component는 관련된 data를 가져오고 이것을 다른 Middleware component에서 HttpContext객체를 통해 사용 가능하도록 만들게 됩니다. Seesion을 사용한다는 것은 Server에 Application data가 남아있고 이를 식별하는 식별자만이 Web Browser에 전송됨을 의미합니다.

 

(1) Session Service와 Middleware 구성하기

 

Seesion을 설정하려면 우선 Service를 구성하고 요청 Pipeline에 Middleware Component를 추가해야 합니다. 아래 예제는 Program.cs에 구문을 추가하여 예제 Application을 위한 Session을 설정하고 이전에 만든 Endpoint를 삭제한 것입니다.

using System.Net.NetworkInformation;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(opts => {
    opts.IdleTimeout = TimeSpan.FromMinutes(30);
    opts.Cookie.IsEssential = true;
});

var app = builder.Build();

app.UseSession();
app.UseMiddleware<MyWebApp.Infrastructure.ConsentMiddleware>();

app.Run();

Seesion을 사용하기로 했다면 관련 data를 어떻게 저장할지를 결정해야 합니다. 이를 위해 ASP.NET Core는 각각 Service를 등록하기 위한 자체 Method인 3가지의 option을 제공하고 있습니다.

AddDistributedMemoryCache 이 Method는 in-memory cache를 설정합니다. cache는 분산되지 않으며 단지 생성된 ASP.NET Core runtime Instance에 대한 data저장만을 담당합니다.
AddDistributedSqlServerCache 이 Method는 SQL Server에 data저장을 위한 cache를 설정하는데 다만 Microsoft.Extensions.Caching.SqlServer package가 설치된 경우에 한하여 사용할 수 있습니다.
AddStackExchangeRedisCache 이 Method는 Redis cache를 설정하며 Microsoft. Extensions.Caching.Redis package를 설치해야 합니다.

예제에서는 in-memory cache를 설정하기 위해 AddDistributeMemoryCache() Method를 사용하였습니다. 해당 Method를 통해 생성된 Cache Service는 분산되지 않으며 Session data를 ASP.NET Core runtime의 단일 Instance에 저장합니다. 때문에 Runtime의 다중 instance 배포를 통해 Application을 확장하는 경우라면 in-memory cache대신 다른 option을 선택해야 합니다.

 

그다음 options pattern을 통해 AddSeesion()를 사용하여 Session Middleware를 구성하였습니다. 이때 사용 가능한 Session에 대한 option class는 SessionOptions이며 아래 표를 통해 Seesion이 정의하는 주요 속성을 확인할 수 있습니다.

Cookie 이 속성은 Seesion Cookie를 설정하는데 사용됩니다.
IdleTimeout 이 속성은 Seesion이 만료된 이 후 Time Span을 설정합니다.

Cookie속성은 Seesion Cookie를 설정하는 데 사용되는 개체를 반환하는데 아래 표는 Seesion Data를 위한 유용한 Cookie 설정 속성을 보여주고 있습니다.

HttpOnly 이 속성은 Browser가 Javascript Code에 의해 보내진 HTTP요청에서 포함되는 Cookie를 차단할 것인지의 여부를 지정합니다. Seesion에 요청을 포함해야 하는 JavaScript application을 사용하는 Project라면 이 속성을 true로 설정해야 합니다. 기본값은 true입니다.
IsEssential 이 속성은 Application의 기능을 위해 Cookie사용을 필요로 하는지 여부를 설정하며 사용자가 Application에서 Cookie사용을 원하지 않는다고 지정한 경우에도 사용되어야 합니다. 기본값은 false입니다.
SecurityPolicy 이 속성은 Cookie에 대한 보안정책을 설정하는데 이때 CookieSecurePolicy 열거형값을 사용합니다.
- Always : 이 값은 HTTPS 요청으로 Cookie사용을 제한합니다.
- SameAsRequest : 이 값은 본래 HTTPS를 사용한 요청인 경우 HTTPS로 Cookie사용을 제한합니다.
- None : 이 값은 HTTP와 HTTPS모두에서 Cookie사용을 가능하게 합니다. 기본값은 None입니다.

예제에서 Option은 Session Cookie가 Javascript로부터의 요청에 포함될 수 있도록 하였으며 Cookie를 필수로 표시하여 사용자가 Cookie를 사용하지 않는다는 것을 표시한 경우에도 사용될 수 있도록 하였습니다. 또한 IdleTimeout option의 설정으로 Seesion Cookie를 포함한 요청을 30분 동안 수신하지 않으면 Cookie가 만료되도록 하였습니다.

Seesion Cookie는 기본적으로 필수로 표현되지 않는데 이 것은 Cookie동의가 사용될 때 문제를 야기할 수 있습니다. 예제에서는 IsEssential속성을 true로 설정하여 Seeion이 항상 동작할 수 있도록 하였는데 만약 예상과 달리 Seesion이 동작하지 않는다는 것을 알게 된다면 이것이 원인일 수 있으므로 IsEssential속성인 true인지 여부를 확인하거나 Application이 동의하지 않고 session cookie를 받아들이지 않는 사용자를 다룰 수 있도록 조정해야 합니다.

마지막으로 session middleware component를 Pipeline에 추가하여 Session Method를 수행합니다. middleware가 Seesion Cookie를 포함한 요청을 처리할 때 Seesion data를 Cache로부터 가져오고 HttpContext객체를 통해 요청이 Pipeline사이로 전달되기 전에 사용 가능하도록 한 후 다른 Middleware component로 이를 제공하게 됩니다. 요청이 Seesion Cookie 없이 도달하면 새로운 Seesion이 시작되고 Cookie가 응답에 추가되므로 다음 요청이 Session의 일부가 되어 식별될 수 있습니다.

 

(2) Seesion 사용하기

 

session middleware는 HttpContext객체의 Session속성을 통해 요청과 관련된 Seesion으로의 접근을 제공합니다. Session속성은 ISession interface의 구현 객체를 반환하며 아래 표의 Method를 통해 Session Data로 접근할 수 있습니다.

Clear() 이 Method는 전체 Session Data를 삭제합니다.
CommitAsync() 이 비동기 Method는 변경된 Session Data를 Cache로 Commint합니다.
GetString(Key) 이 Method는 특정 Key값을 통해 String형식의 값을 가져옵니다.
GetInt32(Key) 이 Method는 특정 Key값을 통해 int형식의 값을 가져옵니다.
Id 이 속성은 Session의 고유한 식별자를 반환합니다.
IsAvailable 이 속성은 Session Data가 정상적으로 Load되었을때 true를 반환합니다.
Keys 이 속성은 Session Data Item의 Key값을 열거합니다.
Remove(Key) 이 Method는 지정된 Key와 관련된 값을 삭제합니다.
SetString(Key, Value) 이 Method는 지정한 Key를 사용해 string형식의 값을 저장합니다.
SetInt32(Key, Value) 이 Method는 지정한 Key를 사용해 int형식의 값을 저장합니다.

Session Data는 키와 값의 쌍으로 저장되며 이때 Key는 문자열, 값은 문자열 혹은 정수형이 될 수 있습니다. 이와 같은 간단한 Data구조는 Session Data를 각각의 Cache에 쉽게 저장할 수 있도록 합니다. 더 복잡한 형태의 Data저장이 필요한 경우 serialization을 사용할 수도 있습니다.

 

아래 예제는 Session Data를 사용해 counter예제는 재작성한 것입니다.

app.UseMiddleware<MyWebApp.Infrastructure.ConsentMiddleware>();

app.MapGet("/session", async context => {
    int counter1 = (context.Session.GetInt32("counter1") ?? 0) + 1;
    int counter2 = (context.Session.GetInt32("counter2") ?? 0) + 1;

    context.Session.SetInt32("counter1", counter1);
    context.Session.SetInt32("counter2", counter2);

    await context.Session.CommitAsync();
    await context.Response.WriteAsync($"Counter1: {counter1}, Counter2: {counter2}");
});

app.Run();

예제에서 GetInt32() Method는 counter1과 counter2 Key와 관련된 값을 읽는 데 사용되었습니다. 여기서 만약 session으로의 최조의 요청이 도달한 경우라면 값을 읽을 수 없는 상태일 수 있으며 이럴 때는 Null 병합 연산자를 사용해 초기값을 제공하게 됩니다. 증가된 값은 SetInt32() Method를 통해 저장되고 Client로의 응답을 생성하는 데 사용됩니다.

 

CommitAsync() Method의 사용은 선택적이지만 Session Data가 Cache에 저장될 수 없는 경우 예외를 throw 하게 되므로 되도록 사용할 것을 권장합니다. 기본적으로 Cache에 문제가 생긴 경우에는 어떠한 Error로 보고되지 않는데 이 것은 뜻하지 않는 동작을 유발할 수 있기 때문입니다.

 

Session Data에 대한 모든 변경은 응답이 Client로 전송되기 전에 만들어져야 합니다. 때문에 Response.WriteAsync Method를 호출하기 전에 Seesion Data를 읽고, 변경하고, 저장하는 등 Session Data에서 필요한 모든 작업을 미리 수행하였습니다.

 

위 예제의 구문은 Session Cookie를 직접적으로 다루거나 혹은 Session 만료 여부의 확인하거나 Cache로부터 Seesion Data를 load 하는 구문이 존재하지 않는다는 점에 주목하시기 바랍니다. HttpContext.Session속성을 통해 결과를 제공하는 Seesion Middleware에 의해서 모든 작업은 자동적으로 수행됩니다. 이러한 접근방식을 통해 발생할 수 있는 여러 결과 중 하나는 session middleware가 요청을 처리할 때까지 HttpContext.Session속성이 Data로 채워지지 않는다는 것입니다. 때문에 UseSession Method가 호출된 이후 요청 Pipeline에 추가된 Middleware나 Endpoint에서만 Seesion Data로의 접근을 시도해야 합니다.

 

Project를 실행하여 /session URL을 요청하면 Counter의 값을 볼 수 있습니다. 그리고 Browser를 새 로고 침하여 값이 증가되는지를 확인합니다.

당연한 이야기지만 Session과 Session Data는 Project를 종료하면 사라지게 됩니다. Session의 저장 방식을 in-memory로 했기 때문인데 다른 저장 방식은 ASP.NET Core Runtime외부에서 작동하므로 application이 재시작되어도 Session을 유지할 수 있습니다.

 

4. HTTPS 연결

 

Web Application에서 HTTPS연결을 사용할 것으로 기대하는 사용자의 수는 점점 더 늘어나고 있으며 민감한 데이터를 포함하지 않거나 반환하지 않는 경우에서도 마찬가지입니다. ASP.NET Core는 HTTP와 HTTPS연결성 모두를 지원하고 있으며 HTTP Client를 HTTPS로 강제 사용하도록 하는 Middleware 또한 제공하고 있습니다.

HTTPS는 HTTP와 TLS(Transport Layer Security) 또는 SSL(Secure Sockets Layer)의 결합입니다.. TLS는 고전적인 SSL를 대체하는 방식이지만 SSL이라는 용어 자체가 Network 보안과 같은 의미로 사용되고 있으며 심지어는 TLS가 적용되고 있는 상황에서도 SSL이라는 용어가 사용되곤 합니다.

보안과 암호화에 관심이 있는 경우라면 HTTPS에 대해서도 같이 알아볼 것을 권장합니다. HTTPS - Wikipedia

 

HTTPS - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Extension of the HTTP communications protocol to support TLS encryption Hypertext Transfer Protocol Secure (HTTPS) is an extension of the Hypertext Transfer Protocol (HTTP). It is used

en.wikipedia.org

(1) HTTPS 연결 설정

 

HTTPS는 Properties folder에 있는 launchSettings.json file에서 사용 설정 및 기타 구성 설정을 수행할 수 있습니다.

"profiles": {
"MyWebApp": {
    "commandName": "Project",
    "dotnetRunMessages": true,
    "launchBrowser": true,
    "applicationUrl": "http://localhost:5232;https://localhost:5233",
    "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
    }
},
"IIS Express": {
    "commandName": "IISExpress",
    "launchBrowser": true,
    "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development"
    }
}
}

applicationUrl은 Application이 응답할 URL을 설정하는 것이며 HTTPS는 HTTPS URL을 설정에 추가하여 사용할 수 있습니다. 이때 URL은 semicolon으로 분리되며 공백은 허용하지 않습니다.

 

.NET Core runtime은 Web Application의 test를 위한 test 인증서를 포함하고 있으며 HTTPS요청에 사용됩니다. test 인증서는 아래 명령을 통해 재생성할 수 있으며 test 인증서를 신뢰하는 것으로 설정할 수 있습니다.

dotnet dev-certs https –-clean
dotnet dev-certs https --trust

다음 화면에서는 기존 인증서를 삭제하고 새로운 인증서로 대체할지 여부를 선택하며

이어서 현재 인증서를 신뢰하도록 합니다.

(2) HTTP 요청 감지하기

 

HTTPS로의 요청은 HttpRequest.IsHttps속성을 통해 판단할 수 있습니다. 다음은 예제를 통해 fallback에 Message를 추가하여 HTTPS를 통한 요청인지 아닌지를 표시하도록 하였습니다.

app.MapFallback(async context => {
    await context.Response.WriteAsync($"HTTPS Request: {context.Request.IsHttps} \n");
    await context.Response.WriteAsync("Hello World!");
});

app.Run();

Project를 시작한 후 http와 https로의 URL을 번갈아 요청하여 다음과 비슷한 결과가 표시되는지를 확인합니다.

http
https

(3) HTTPS 요청 강제하기

 

ASP.NET Core는 HTTP 요청이 도달하는 경우 Redirection을 통해 HTTPS 사용을 강제하는 Middleware component를 제공하고 있습니다. 아래 예제는 요청 Pipeline에 해당 Middleware를 추가하는 방법을 보여주고 있습니다.

var app = builder.Build();

app.UseHttpsRedirection();
app.UseSession();

예제에서는 UseHttpsRedirection() Method를 예제와 같은 위치에서 호출하여 요청이 시작되는 부분에 Middleware를 추가하도록 하였습니다. 따라서 다른 구성요소가 Pipeline을 단락 하고 일반적인 HTTP 응답을 생성하기 전에 HTTPS로의 Redirection을 발생시키게 됩니다.

options pattern은 AddHttpsRedirection Method를 아래와 같이 호출함으로써 HTTPS Reidrection Middleware를 구성하는 데에도 사용될 수 있습니다.
builder.Services.AddHttpsRedirection(opts => {
	opts.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
	opts.HttpsPort = 443;
});
여기에서는 2가지의 구성 option만이 표시되는데 설정 file로부터 load 된 값을 overriding 함으로써 redirection응답을 통해 상태 code와 client가 Redirect 될 Port를 설정하고 있습니다. 특히 HTTPS Port를 지정하는 것은 Application을 배포할 때 유용하게 사용될 수 있으나 Redirection 상태 code를 바꾸는 경우에는 주의가 필요합니다.

Project를 실행하여 아래와 같은 결과가 표시되는지를 확인합니다.

HTTPS redirection middleware는 HTTP 요청을 가로채어 HTTPS로의 Redirection을 수행할 것입니다.

어떤 경우에는 WebBrowser가 HTTPS scheme를 감추는 경우가 있는데 이런 경우 Port번호를 대신 확인하여 HTTPS로의 Redirection이 잘 수행되었는지 확인해 볼 수 있습니다. 대개의 경우 URL 주소 입력 칸을 Click 하게 되면 WebBrowser는 Full URL을 표시하게 되는데 이때에도 HTTPS여부를 확인할 수 있습니다.

(4) HTTP Strict Transport Security

 

HTTPS Redirection의 한계 중 하나는 사용자가 보안 연결로 Redirect가 이루어지기 전에 HTTP를 사용한 초기 요청을 시도할 수 있다는 위험성을 가졌다는 것입니다.

 

HSTS(HTTP Strict Transport Security) Protocol은 이러한 위험성을 완화하도록 고안되었으며 Browser에게 Web Application host로 요청을 보낼 때만 HTTPS를 사용하도록 하는 Header를 추가하여 작동합니다. HSTS Header가 수신되고 나면 HSTS를 지원하는 Browser는 사용자가 고의적으로 HTTP사용을 시도한다고 하더라도 HTTPS를 사용하여 요청을 보내도록 합니다. 아래 예제는 HSTS Middleware를 요청 Pipeline에 추가하는 방법을 보여주고 있습니다.

builder.Services.AddHsts(opts => {
    opts.MaxAge = TimeSpan.FromDays(1);
    opts.IncludeSubDomains = true;
});

var app = builder.Build();

if (app.Environment.IsProduction())
{
    app.UseHsts();
}

예제에서 Middleware는 UseHsts() Method를 통해 요청 Pipeline에 추가되었으며 AddHsts Method를 사용하여 설정되었습니다. 아래 표에서는 AddHsts Method에서 사용 가능한 속성을 나열하였습니다.

ExcludeHosts 이 속성은 Middleware가 HSTS Header를 보내지 않는 Host를 가진 List<string>형식을 반환합니다. 기본적으로 Localhost와 IP version 4와 version 6에 해당하는 loopback address는 제외됩니다.
IncludeSubDomains 이 속성이 true이면 Browser는 HSTS를 하위 도메인에도 적용합니다. 기본값은 false입니다.
MaxAge 이 속성은 Browser가 HTTPS요청만을 수행애야 하는 기간을 설정합니다. 기본값은 30일입니다.
Preload 이 속성은 HSTS preload scheme의 일부인 domains을 위해 true로 설정됩니다. domain은 Browser에 hard-code되며 초기 비보안 요청을 회피하고 HTTPS의 사용만을 보장하게 됩니다. hstspreload.org를 살펴보면 좀 더 자세한 사항을 알 수 있습니다.

HSTS는 개발단계에서는 사용되지 않으며 Production환경에서만 사용되므로 예제에서도 UseHsts() Method는 해당 환경에서만 호출됩니다.

 

다만 HSTS는 사용에 주의가 필요한데 client가 Application에 접근할 수 없는 상황을 만들기 쉽기 때문입니다. 특히 HTTP와 HTTPS에서 비표준 port를 사용하는 경우에는 더욱 그렇습니다. 만약 server이름이 myweb인 곳에 Application이 배포되었다고 가정한다면 사용자는 http://myweb:5000과 같은 URL을 요청할 수 있고 Browser는 https://myweb:5100으로 redirect한뒤 HSTS Header를 보내는 정상적인 동작을 수행할 것입니다. 그러나 다음 사용자가 다시 http://myweb:5000으로의 요청을 시도한다면 보안 연결이 성립될 수 없다는 Error를 보게 될 수 있습니다.

 

이러한 문제가 발생하는 이유는 일부 Browser의 경우 HSTS로 지나치게 단순화한 접근법을 사용하고 있기 때문이며 HTTP는 80번, HTTPS는 443 Port로 다뤄질 것임을 추정하고 있기 때문입니다.

 

사용자가 http://myweb:5000으로 요청할 때 Browser는 HSTS data를 확인하고 이전에 수신된 myweb에 대한 HSTS header를 확인하게 됩니다. 그리고 Browser는 사용자가 입력한 HTTP URL 대신 https://myweb:5000으로 요청을 보내게 되는데  ASP.NET Core는 HTTP Post를 통해서는 HTTPS를 다루지 않으며 이전에 수신한 5100으로의 redirection을 고려하지 않으므로 요청은 곧 실패하게 되는 것입니다.

 

이것은 HTTP에서 80 port를 사용하거나 HTTPS에서 443 port를 사용하는 경우에는 문제가 되지 않습니다. http://myweb은 http://myweb:80과 동일하며 https://myweb은 https://myweb:443과 동일합니다.

 

일단 Browser가 HSTS Header를 수신하게 되면 Header의 MaxAge기간 동안은 계속 사용하게 됩니다. Application을 처음 배포하는 경우에는 HTTPS가 정확히 작동한다는 확신이 생길 때까지 HSTS의 MaxAge속성을 비교적 짧게 설정하는 것이 좋습니다. 이것은 예제에서 해당 속성의 설정값을 1(하루)로 지정한 이유이기도 합니다. client가 더 이상 HTTP 요청이 필요하지 않다는 확신이 생긴다면 그때 MaxAge값을 늘리는 것이 좋으며 보통 1년 정도를 많이 설정하는 편입니다.

google chrome을 통해 HSTS를 test 하고자 한다면 주소창에 'chrome://net-internals/#hsts'을 입력하여 HSTS가 적용된 domain list를 확인하고 편집할 수 있습니다.

4. 예외 및 Error 다루기

 

요청 Pipeline이 생성되면 WebApplicationBuilder class는 개발환경을 사용하여 개발자에게 도움이 되는 HTTP 응답을 생성함으로써 예외를 다루는 Middleware를 활성화합니다. 아래 code는 WebApplicationBuilder class의 일부를 나타낸 것입니다.

if (context.HostingEnvironment.IsDevelopment()) {
	app.UseDeveloperExceptionPage();
}

위 code에서 UseDeveloperExceptionPage Method는 예외를 가로채 더욱 도움이 될 수 있는 상세한 응답을 표현하는 Middleware component를 추가합니다. 예외가 처리되는 방식을 알아보기 위해 이전 이전 예제에서 사용된 middleware와 endpoint를 고의적으로 예외를 발생시키는 새로운 component로 아래와 같이 대체하였습니다.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.Run(context => {
    throw new Exception("Exception!!");
});

app.Run();

Project를 실행하면 아래와 같이 Middleware component가 생성한 결과를 볼 수 있습니다.

Page는 stack추적과 함께 Cookie와 Header에 대한 상세를 포함하여 요청에 대한 자세한 정보들도 표현하고 있습니다.

 

(1) HTML Error Response

 

개발자 예외 Middleware를 사용하지 않는 경우(Application이 운영 중일 때처럼) ASP.NET Core는 Error Code만을 포함한 응답을 보냄으로써 처리되지 않는 예외를 다루게 됩니다. 아래 예제는 Application의 동작 환경이 Production으로 변경된 경우를 표시하고 있습니다.

"profiles": {
"MyWebApp": {
    "commandName": "Project",
    "dotnetRunMessages": true,
    "launchBrowser": true,
    "applicationUrl": "http://localhost:5232;https://localhost:5233",
    "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Production"
    }
},

위와 같이 설정한 후 Project를 실행하면 아래와 같은 응답을 보게 됩니다.

이러한 응답이 표시되는 이유는 ASP.NET Core가 어떠한 내용도 없이 상태 code 500을 포함한 응답을 제공함으로써 Browser가 자체 오류를 표시했기 때문입니다.

 

상태 code를 반환하는 대안으로 ASP.NET Core는 처리되지 않은 예외를 가로채고 Browser로 Redirection을 보냄으로써 상태 code를 그대로 보여주는 대신 좀 더 친숙한 오류 화면을 표시할 수 있도록 하는 Middleware를 제공하고 있습니다. 예외 Redirection Middleware는 UseExceptionHandler Method를 통해 아래와 같이 추가됩니다.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/error.html");
    app.UseStaticFiles();
}

예외가 발생하게 되면 exception handler middleware는 응답을 가로채고 UseExceptionHandler Method의 매개변수로 전달한 URL로 Browser를 Redirect 하게 됩니다. 예제에서는 정적 file로 처리될 URL로 Redirection을 수행하고 있는데 이를 위해 UseStaticFiles Middleware 또한 Pipeline에 추가하였습니다.

 

따라서 error.html이라는 이름의 file을 아래 내용으로 wwroot folder에 추가합니다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>에러</title>
</head>
<body>
    에러!!
</body>
</html>

Project를 다시 실행하면 이전에 오류 code만을 표시하는 대신 위에서 추가한 HTML Page를 Error화면으로 표시할 것입니다.

UseExceptionHandler Method는 위의 결과보다 더 복합적인 응답을 할 수 있는 다른 version도 존재하지만 가능한 한 단순한 형태의 응답을 사용하는 것이 좋습니다. Application에서 마주하게 될 모든 문제를 예측할 수 없고 handler를 trigger 한 예외를 처리하려고 시도할 때 다른 예외를 마주하게 됨으로써 응답 자체가 혼란스러워지거나 아예 아무런 응답도 하지 않게 되는 위험성이 있기 때문입니다.

 

(2) 상태 코드 응답 개선하기

 

모든 Error응답이 예외를 탐지하지 않은 결과인 것은 아닙니다. 몇몇 요청은 software결함을 제외하고서라도 지원되지 않는 URL이나 인증이 필요한 경우 등의 이유로 처리되지 않을 수 있습니다. 이러한 상황에서 client를 다른 URL로 이동시키는 것은 문제가 될 수 있습니다. 왜냐하면 일부 client의 경우 문제점을 파악하기 위해 error code에 의존하기 때문입니다.

 

ASP.NET Core는 이에 대한 Middleware를 제공하여 Redirection 없이도 사용자 친화적인 content안에 error 응답을 추가시킬 수 있습니다. 따라서 error 상태 코드를 유지함과 동시에 사용자가 문제를 이해하는데 도움을 줄 수 있는 친숙한 message를 제공할 수 있습니다.

 

이를 위한 가장 간단한 접근방법은 HTML응답 자체를 string으로 정의하는 것인데 다소 불편하고 어색한 방법일 수 있지만  간단하면서도 문제를 처리하는데 더 선호되는 방법이기도 합니다. 예제를 위한 HTML응답을 만들기 위해 HTMLResponse.cs라는 이름의 File을 Infrastructure folder에 아래와 같이 생성합니다.

namespace MyWebApp.Infrastructure
{
    public class HTMLResponse
    {
        public static string Response = @"
            <!DOCTYPE html>
            <html>
                <head>
                    <meta charset=""utf-8"">
                    <title>Error</title>
                </head>
                <body>
                    <h3>Error {0}</h3>
                    <h6>오류가 발생하였습니다. 다시 <a href=""/"">메인페이지</a>로 되돌아 가거나 문제가 지속되면 관리자에게 문의하시기 바랍니다.</h6>
                </body>
            </html>";
    }
}

예제에서는 Response라는 속성을 정의하고 있는데 여기에는 HTML응답을 위한 문자열 전체를 포함하고 있습니다. 또한 Error에는 '{0}'이라는 Placeholder를 볼 수 있는데 해당 응답이 client에 보내질 때 이 부분에 응답 상태 code가 추가될 것입니다.

 

따라서 아래와 같이 상태 code Middleware를 요청 Pipeline에 추가하고 요청한 Page를 찾을 수 없음을 나타내는 404 code를 반환할 새로운 Middleware component를 추가합니다.

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/error.html");
    app.UseStaticFiles();
}

app.UseStatusCodePages("text/html", MyWebApp.Infrastructure.HTMLResponse.Response);

app.Use(async (context, next) => {
    if (context.Request.Path == "/error") {
        context.Response.StatusCode = StatusCodes.Status404NotFound;
        await Task.CompletedTask;
    }
    else
        await next();
});

app.Run(context => {
    throw new Exception("Exception!!");
});

UseStatusCodePages Method는 위에서 추가한 응답 Middlware를 요청 Pipeline에 추가하는 Method인데 첫 번째 인수로 응답의 Content-Type header에 사용될 값을 지정합니다. 예제에서는 text/html로 지정하였습니다. 두 번째 인수로는 응답에 사용될 본문을 지정하는 것인데 이에 대한 값으로 위에서 만든 HTMLResponse의 Response속성을 사용하였습니다.

 

그다음 밑에서 추가한 사용자 Middleware component에서는 응답의 상태 code를 특정하기 위해 HttpResponse.StatusCode 속성을 설정하고 있으며 이때 StatusCode class를 사용하여 값을 정의하고 있습니다. Middleware component는 Task의 반환을 필요로 합니다. 예제에서는 특히 Task.CompletedTask 속성을 사용했는데 해당 Middleware component에서는 수행할 작업이 아무것도 없기 때문입니다.

 

404 상태 code가 처리되는지를 확인하기 위해 Project를 실행하여 /error로 URL을 요청합니다. 상태 code Middleware는 응답을 가로채 아래와 같은 응답을 추가할 것입니다. 이때 UseStatusCodePages Method에서 두 번째 인수로 사용된 string에서는 내부의 placeholder를 resolve 하기 위해 상태 code를 사용해 보간 처리하게 됩니다.

상태 code middleware는 단지 400에서 600 사이의 상태 code만 반환하며 이전에 포함한 content를 대체하지 않습니다. 다시 말해 다른 Middleware component에서 응답을 생성하기 시작한 이후 오류가 발생했다면 위와 같은 응답을 볼 수 없게 됩니다. 예외의 경우 요청이 Pipeline을 통과하는 흐름에 방해를 주기 때문에 상태 code Middleware는 이것이 client에게 응답을 제공하기 전 아예 응답을 확인할 수 있는 기회가 주어지지 않을 수 있고 따라서 상태 code middleware는 처리되지 않은 예외에 대해서는 응답을 수행할 수 없게 됩니다. 따라서 UseStatusCodePages Method는 UseExceptionHandler 혹은 UseExceptionHandler Method와 함께 사용됩니다.

UseStatusCodePagesWithRedirects와 UseStatusCodePagesWithRedirects라는 2개의 관련된 Method도 존재하는데 이 Method는 client를 다른 URL로 Redirection 하거나 요청을 Pipeline을 통해 다른 URL로 rerunning 하게 되므로 본래 상태 code를 손실하게 됩니다.

5. Host Header를 통한 요청 filtering 하기

 

HTTP 명세(specification)는 Host header를 포함한 요청이 필요하며 여기서 Host header는 해당 요청을 보낸 hostname을 특정하고 있습니다. 이를 통해 하나의 HTTP Server는 단일 port에서 HTTP 요청을 수신하고 요청된 hostname에 따라 요청을 다르게 처리하는 가상 server를 지원할 수 있습니다.

 

Host header를 기반으로 요청을 Filter 하는 Middleware를 포함하여 기타 대부분의 Middleware 기본 설정은 Program class에 의해 요청 Pipeline으로 추가됩니다. 따라서 인가된 hostname을 목표로 한 요청만이 처리되고 그 외 다른 요청은 반려됩니다.

 

Host header Middleware의 기본 설정은 appsettings.json file에 다음과 같이 포함되어 있습니다.

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "AllowedHosts": "*",
    "Member": {
        "MemberName": "Kim"
    }
}

여기서 AllowdHosts가 해당 속성이며 Project가 생성될 때 JSON file에 추가됩니다. 해당 설정의 기본값은 *로 Host header의 값과는 상관없이 모든 요청을 수용하도록 되어 있고 JSON file을 수정하여 조정하거나 다음과 같이 option pattern을 통해 설정이 재조정될 수 있습니다.

기본적으로 Middleware는 Pipeline에 추가되지만 Middleware를 명시적으로 추가하고자 한다면 UseHostFiltering() method를 사용할 수 있습니다.
using Microsoft.AspNetCore.HostFiltering;

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<HostFilteringOptions>(opts => {
    opts.AllowedHosts.Clear();
    opts.AllowedHosts.Add("*.cliel.com");
});

var app = builder.Build();

HostFilteringOptions class는 아래 표의 속성을 사용해 Host filtering middleware를 설정하는 데 사용됩니다.

AllowedHosts 이 속성은 요청이 허용된 domain을 포함한 List<string>형식을 반환합니다. *은 전체를 허용하는 것이 되므로 *.cliel.com은 cliel.com의 모든 domain을 허용한다는 뜻이 됩니다.
AllowEmptyHosts 이 속성이 false이면 Middleware는 Host header를 포함하지 않는 모든 요청을 반려하게 됩니다. 기본값은 true입니다.
IncludeFailureMessage 이 속성이 true이면 응답에 message를 포함하여 error의 이유를 나타내게 됩니다. 기본값은 true입니다.

예제에서는 우선 Clear() Method를 호출하여 appSettings.json file로부터 load 된 설정된 값을 모두 삭제하도록 한 뒤 Add() Method를 통해 cliel.com domain의 모든 host에 대해서만 요청이 수용되도록 하였습니다. 이 설정으로 Browser에서 Localhost로 보내진 모든 요청은 더 이상 수용 가능한 Host header를 포함하지 않게 됩니다.(cliel.com에서 보내는 요청이 아니므로) 따라서 Project를 실행하여 Browser로 요청을 보내면 다음과 같은 결과를 보게 됩니다.

Host header Middleware는 요청에서 host header를 확인하여 요청 hostname이 AllowedHosts list와 일치하는지의 여부를 결정하고 잘못된 요청을 나타내는 400 상태 code를 통해 요청을 반려시키게 됩니다.

728x90