ASP.NET Core의 세션 및 응용 프로그램 상태

등록일시: 2017-09-11 08:00,  수정일시: 2017-11-22 05:18
조회수: 9,203
이 문서는 ASP.NET Core 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 여러 요청 간에 응용 프로그램 및 세션 상태를 유지하는 몇 가지 방법에 관해서 설명하고 세션 공급자를 설정하는 방법과 간단한 사용 방법을 살펴봅니다.

HTTP는 상태를 관리하지 않는 프로토콜입니다. 웹 서버는 각각의 HTTP 요청을 서로 상관 없는 개별적인 요청으로 간주하며, 이전 요청의 사용자 값을 남겨두지 않습니다. 본문에서는 여러 요청 간에 응용 프로그램 및 세션 상태를 유지하는 몇 가지 방법을 살펴봅니다.

세션 상태

세션 상태는 사용자가 웹 응용 프로그램을 사용하는 동안 사용자의 데이터를 저장하고 보관할 수 있는 ASP.NET Core의 기능입니다. 세션 상태는 서버 측에 사전이나 해시 테이블의 형태로 구성되며 브라우저의 여러 요청들 간에 데이터를 유지합니다. 참고로 세션 데이터는 캐시의 지원을 받습니다.

ASP.NET Core는 클라이언트에게 세션 ID가 담긴 쿠키를 전달해서 세션 상태를 유지하며, 이 쿠키는 매 요청마다 다시 서버로 전송됩니다. 그러면 서버는 쿠키에 담긴 세션 ID를 이용해서 다시 세션 데이터를 조회합니다. 세션 쿠키는 브라우저에 한정되므로 브라우저 간에 세션을 공유할 수는 없습니다. 세션 쿠키는 브라우저의 세션이 종료될 때만 삭제됩니다. 만료된 세션의 쿠키가 수신되면 동일한 세션 쿠키를 사용하는 새 세션이 생성됩니다.

서버는 마지막 요청 이후 제한된 시간 동안만 세션을 유지합니다. 기본값인 20 분을 그대로 사용하거나 세션 만료 시간을 설정할 수 있습니다. 세션 상태는 영구적으로 유지할 필요가 없는 특정 세션과 관련된 사용자 데이터를 저장하기에 이상적입니다. Session.Clear를 호출하거나 데이터 저장소에서 세션이 만료되면 데이터가 보조 저장소에서 삭제됩니다. 그러나 서버는 브라우저가 닫히거나 세션 쿠키가 삭제된 시점을 알 수 없습니다.

경고

민감한 데이터는 세션에 저장하지 마십시오. 클라이언트가 브라우저는 닫지 않고 세션 쿠키만 지울 수도 있습니다 (일부 브라우저는 창 간에도 세션 쿠키를 유지합니다). 또한 세션이 단일 사용자에게 제한되지 않고 다음 사용자가 동일한 세션을 계속 사용할 수 있는 경우도 있습니다.

메모리 내 세션 공급자(In-Memory Session Provider)는 세션 데이터를 로컬 서버에 저장합니다. 따라서 서버 팜에서 웹 응용 프로그램을 실행하려면 고정 세션(Sticky Sessions)을 사용해서 각 세션을 특정 서버에 연결해야 합니다. 기본적으로 Windows Azure 웹 사이트 플랫폼은 고정 세션으로 설정됩니다 (ARR, 응용 프로그램 요청 라우팅). 그러나 고정 세션은 확장성에 영향을 줄 수 있으며 웹 응용 프로그램의 갱신을 복잡하게 만들 수 있습니다. 더 좋은 방식은 고정 세션이 필요 없는 Redis나 SQL Server 분산 캐시를 사용하는 것입니다. 보다 자세한 정보는 Working with a Distributed Cache 문서를 참고하시기 바랍니다. 서비스 공급자 설정에 관한 내용은 본문 뒷부분의 세션 구성하기 절에서 자세히 살펴봅니다.

이번 절의 나머지 부분에서는 사용자 데이터를 저장할 수 있는 여러 가지 방법에 대해서 살펴보겠습니다.

TempData

ASP.NET Core MVC는 Controller 클래스에서 TempData 속성을 노출합니다. 이 속성은 데이터가 읽혀지기 전까지만 데이터를 저장합니다. 그러나 Keep 메서드나 Peek 메서드를 이용해서 데이터를 삭제하지 않고 값을 확인할 수도 있습니다. TempData는 하나 이상의 요청이 연이어지는 재지정에서 데이터가 필요한 경우 특히 유용합니다. TempData는 세션 상태를 기반으로 구현됩니다.

ASP.NET Core 1.1 이상에서는 사용자의 TempData를 쿠키에 저장하는 쿠키 기반 TempData 공급자를 사용할 수 있습니다. 쿠키 기반 TempData 공급자를 사용하려면 ConfigureServices 메서드에서 CookieTempDataProvider 서비스를 등록하면 됩니다:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    // Add CookieTempDataProvider after AddMvc and include ViewFeatures.
    // using Microsoft.AspNetCore.Mvc.ViewFeatures;
    services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
}

쿠키에 저장되는 데이터는 Base64UrlTextEncoder를 이용해서 인코딩됩니다. 쿠키가 암호화 및 청크 분할되기 때문에 단일 쿠키 크기 제한이 적용되지 않습니다. 암호화 된 데이터를 압축하면 CRIMEBREACH 공격과 같은 보안 문제가 발생할 수 있으므로 쿠키 데이터는 압축되지 않습니다. 쿠키 기반 TempData 공급자에 대한 더 자세한 정보는 CookieTempDataProvider를 참고하시기 바랍니다.

쿼리 문자열

제한된 양의 데이터를 새로운 요청의 쿼리 문자열에 추가해서 한 요청에서 다른 요청으로 데이터를 전달할 수 있습니다. 이 기능은 영구적으로 유지되는 상태를 캡처할때 유용하며, 이를 활용해서 전자 메일 또는 소셜 네트워크를 통해서 공유할 수 있는 상태가 포함된 링크를 만들 수 있습니다. 그러나 바로 그렇기 때문에 민감한 데이터는 쿼리 문자열에 사용하지 말아야 합니다. 쿼리 문자열은 공유만 쉬운게 아니라, 쿼리 문자열에 포함된 데이터를 통해서 크로스 사이트 요청 위조 (CSRF, Cross-Site Request Forgery) 공격을 받을 수도 있으며, 이는 사용자가 인증되어 있는 동안 악의적인 사이트를 방문하도록 유도할 수 있는 공격입니다. 그런 다음, 공격자는 응용 프로그램에서 사용자 데이터를 훔치거나 사용자를 대신해서 악의적인 행동을 취할 수 있습니다. 보존된 모든 응용 프로그램 상태 및 세션 상태는 CSRF 공격을 방어해야 합니다. CSRF 공격에 대한 보다 자세한 정보는 Preventing Cross-Site Request Forgery (XSRF/CSRF) Attacks in ASP.NET Core 문서를 참고하시기 바랍니다.

Post 데이터 및 숨겨진 필드

데이터를 숨겨진 폼 필드에 저장한 다음, 다음 요청에서 다시 게시할 수 있습니다. 이는 여러 페이지로 구성된 양식에서 일반적으로 사용되는 기법입니다. 그러나 잠재적으로 클라이언트가 데이터를 변조할 수도 있기 때문에, 항상 서버에서 데이터의 유효성을 다시 확인해야 합니다.

쿠키

쿠키를 이용하면 웹 응용 프로그램이 사용자 별 데이터를 저장할 수 있습니다. 쿠키는 모든 요청에 포함되어 전송되기 때문에 크기를 최소로 유지해야 합니다. 이상적으로는 서버에 저장된 실제 데이터의 식별자만 쿠키에 저장해야 합니다. 대부분의 브라우저는 쿠키를 4096 바이트로 제한합니다. 또한 각 도메인마다 제한된 수의 쿠키만 사용할 수 있습니다.

쿠키는 변경될 수 있는 데이터이므로 서버에서 유효성을 검사해야 합니다. 클라이언트에 위치한 쿠키의 내구성은 사용자 개입이나 만료에 영향 받지만, 일반적으로 클라이언트 측 데이터 지속성의 가장 견고한 형태입니다.

종종 쿠키는 개인화에 사용되어 사용자에 맞게 콘텐츠가 사용자 정의됩니다. 대부분 사용자는 식별만 되고 인증되지는 않기 때문에, 일반적으로 사용자 이름, 계정 이름 또는 고유 사용자 ID를 (GUID 같은) 쿠키에 저장해서 쿠키를 보호할 수 있습니다. 그런 다음 쿠키를 이용해서 사이트의 사용자 개인 설정 인프라에 접근할 수 있습니다.

HttpContext.Items

Items 컬렉션은 특정 단일 요청을 처리하는 동안에만 필요한 데이터를 저장하기 알맞은 위치입니다. 이 컬렉션의 내용은 각 요청이 처리된 이후에 삭제됩니다. Items 컬렉션은 구성 요소나 미들웨어들이 요청 중 각기 다른 시점에 동작하지만 서로 매개 변수를 전달할 수 있는 직접적인 방법이 딱히 없을 때 서로 통신하기 위한 방법으로 가장 잘 사용됩니다. 더 자세한 내용은 본문의 HttpContext.Items 활용하기 절을 참고하시기 바랍니다.

캐시

캐싱은 데이터를 저장하고 조회하는 효율적인 방법입니다. 시간 및 기타 고려 사항에 기반해서 캐시된 항목의 수명을 제어할 수 있습니다. 캐싱에 관해서 더 자세히 살펴보시기 바랍니다.

세션 구성하기

Microsoft.AspNetCore.Session 패키지는 세션 상태 관리 미들웨어를 제공합니다. 세션 미들웨어를 사용하려면 Startup 클래스에 다음을 포함시켜야 합니다:

  • IDistributedCache 메모리 캐시 중 한 가지. IDistributedCache 구현은 세션의 백업 저장소로 사용됩니다.
  • AddSession 메서드 호출, 이 메서드를 사용하려면 "Microsoft.AspNetCore.Session" NuGet 패키지가 필요합니다.
  • UseSession 메서드 호출.

다음 코드는 메모리 내 세션 공급자의 설정 방법을 보여줍니다.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        // Adds a default in-memory implementation of IDistributedCache.
        services.AddDistributedMemoryCache();

        services.AddSession(options =>
        {
            // Set a short timeout for easy testing.
            options.IdleTimeout = TimeSpan.FromSeconds(10);
            options.CookieHttpOnly = true;
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseSession();
        app.UseMvcWithDefaultRoute();
    }
}

세션 관리 미들웨어를 설치하고 구성하고 나면 HttpContextSession 속성을 참조할 수 있습니다.

만약 UseSession 메서드를 호출하기 전에 Session 속성에 접근하면 InvalidOperationException: Session has not been configured for this application or request 예외가 발생합니다.

이미 Response 스트림을 쓰기 시작한 뒤에 새로운 Session을 생성하려고 하면 (즉, 세션 쿠키가 만들어지지 않은 경우), InvalidOperationException: The session cannot be established after the response has started 예외가 발생합니다. 이 예외는 웹 서버 로그에서 발견할 수 있으며, 브라우저에는 출력되지 않습니다.

비동기적으로 세션 로딩하기

ASP.NET Core의 기본 세션 공급자는 TryGetValue, Set 또는 Remove 메서드를 호출하기 전에 명시적으로 ISession.LoadAsync 메서드를 호출하는 경우에만, 비동기적으로 기반 IDistributedCache 저장소에서 세션 레코드를 로드합니다. LoadAsync 메서드가 먼저 호출되지 않으면 동기적으로 기반 세션 레코드가 로드되기 때문에 잠재적으로 응용 프로그램의 확장성에 영향을 미칠 수 있습니다.

이 패턴을 응용 프로그램에 강제로 적용하려면, 먼저 LoadAsync 메서드를 호출하지 않고 TryGetValue, Set 또는Remove 메서드를 호출하면 예외를 던지도록 DistributedSessionStoreDistributedSession의 구현을 래핑하십시오. 그리고, 랩핑 된 버전을 서비스 컨테이너에 등록하면 됩니다.

구현 세부 사항

세션은 쿠키를 이용해서 단일 브라우저의 요청을 추적하고 식별합니다. 기본적으로 ".AspNet.Session"이라는 이름의 쿠키를 사용하며 경로는 "/"로 설정됩니다. 기본적으로 쿠키의 도메인은 지정되지 않으며, 페이지의 클라이언트 측 스크립트에서 접근할 수 없습니다 (기본적으로 CookieHttpOnlytrue로 설정됩니다).

세션의 기본 설정을 재지정하려면 SessionOptions를 사용합니다:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    // Adds a default in-memory implementation of IDistributedCache.
    services.AddDistributedMemoryCache();

    services.AddSession(options =>
    {
        options.CookieName = ".AdventureWorks.Session";
        options.IdleTimeout = TimeSpan.FromSeconds(10);
    });
}

서버는 IdleTimeout 속성을 이용해서 세션의 내용을 삭제하기 전에 세션이 유휴 상태로 남아있을 기간을 결정합니다. 이 속성은 쿠키 만료 시간과 독립적입니다. 각 요청이 세션 미들웨어를 통과할 때마다 (읽거나 쓰거나) 만료 시간이 재설정됩니다.

Session잠기지 않기 때문에 (Non-Locking), 두 요청이 동시에 세션 내용을 수정하려고 시도할 경우 마지막 요청의 내용이 첫 번째 요청의 내용을 덮어씁니다. Session일관적 세션(Coherent Session)으로 구현되었기 때문에 모든 내용이 함께 저장됩니다. 따라서 세션의 서로 다른 부분을 (다른 키를) 수정하는 두 요청 역시 서로 영향을 미치게 됩니다.

세션 값 설정하기 및 가져오기

세션은 HttpContextSession 속성을 통해서 접근할 수 있습니다. 이 속성은 ISession의 구현입니다.

다음 예제는 정수와 문자열을 설정하고 가져오는 방법을 보여줍니다:

public class HomeController : Controller
{
    const string SessionKeyName = "_Name";
    const string SessionKeyYearsMember = "_YearsMember";
    const string SessionKeyDate = "_Date";

    public IActionResult Index()
    {
        // Requires using Microsoft.AspNetCore.Http;
        HttpContext.Session.SetString(SessionKeyName, "Rick");
        HttpContext.Session.SetInt32(SessionKeyYearsMember, 3);
        return RedirectToAction("SessionNameYears");
    }
    
    public IActionResult SessionNameYears()
    {
        var name = HttpContext.Session.GetString(SessionKeyName);
        var yearsMember = HttpContext.Session.GetInt32(SessionKeyYearsMember);

        return Content($"Name: \"{name}\",  Membership years: \"{yearsMember}\"");
    }

다음 확장 메서드를 추가하면 직렬화할 수 있는 개체를 세션에 설정하거나 가져올 수 있습니다:

using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

public static class SessionExtensions
{
    public static void Set<T>(this ISession session, string key, T value)
    {
        session.SetString(key, JsonConvert.SerializeObject(value));
    }

    public static T Get<T>(this ISession session,string key)
    {
        var value = session.GetString(key);
        return value == null ? default(T) : 
                              JsonConvert.DeserializeObject<T>(value);
    }
}

다음 예제는 직렬화할 수 있는 개체를 설정하고 가져오는 구체적인 방법을 보여줍니다:

public IActionResult SetDate()
{
    // Requires you add the Set extension method mentioned in the article.
    HttpContext.Session.Set<DateTime>(SessionKeyDate, DateTime.Now);
    return RedirectToAction("GetDate");
}

public IActionResult GetDate()
{
    // Requires you add the Get extension method mentioned in the article.
    var date = HttpContext.Session.Get<DateTime>(SessionKeyDate);
    var sessionTime = date.TimeOfDay.ToString();
    var currentTime = DateTime.Now.TimeOfDay.ToString();

    return Content($"Current time: {currentTime} - "
                 + $"session time: {sessionTime}");
}

HttpContext.Items 활용하기

HttpContext 추상화는 Items라는 IDictionary<object, object> 형식의 사전 컬렉션을 지원합니다. 이 컬렉션은 HttpRequest가 시작됨과 동시에 사용 가능하고 해당 요청이 끝나면 폐기되며, 키 항목에 값을 지정하거나 특정 키의 값을 요청하는 방식으로 사용할 수 있습니다.

다음 예제의 경우, 먼저 특정 미들웨어에서 Items 컬렉션에 isVerified를 추가합니다.

app.Use(async (context, next) =>
{
    // perform some verification
    context.Items["isVerified"] = true;
    await next.Invoke();
});

그런 다음, 파이프라인 상의 다른 미들웨어에서 isVerified에 접근합니다:

app.Run(async (context) =>
{
    await context.Response.WriteAsync("Verified request? " + context.Items["isVerified"]);
});

단일 응용 프로그램에서만 사용하는 미들웨어는 string 키를 사용해도 무방합니다. 그러나 여러 응용 프로그램간에 공유되는 미들웨어는 키가 충돌할 가능성을 피하기 위해서 고유 개체 키를 사용해야합니다. 여러 응용 프로그램에서 동작해야 하는 미들웨어를 개발하고 있다면, 다음과 같이 미들웨어 클래스에 정의된 고유 개체 키를 사용하십시오:

public class SampleMiddleware
{
    public static readonly object SampleKey = new Object();

    public async Task Invoke(HttpContext httpContext)
    {
        httpContext.Items[SampleKey] = "some value";
        // additional code omitted
    }
}

그런 다음, 다른 코드에서는 이 미들웨어 클래스가 노출하는 키를 이용해서 HttpContext.Items에 저장된 값에 접근합니다:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        string value = HttpContext.Items[SampleMiddleware.SampleKey];
    }
}

이 방식의 장점은 코드 여기 저기에서 "마법 문자열"을 반복적으로 사용하는 상황을 피할 수 있다는 것입니다.

응용 프로그램 상태 데이터

모든 사용자가 데이터를 사용할 수 있게 만들려면 의존성 주입을 이용하십시오:

  1. 데이터를 포함하는 서비스를 정의합니다 (예, MyAppData 클래스).
public class MyAppData
{
    // Declare properties/methods/etc.
}
  1. ConfigureServices 메서드에서 서비스 클래스를 추가합니다 (예, services.AddSingleton<MyAppData>();).
  2. 각 컨트롤러에서 추가한 데이터 서비스 클래스를 사용합니다:
public class MyController : Controller
{
    public MyController(MyAppData myService)
    {
        // Do something with the service (read some data from it, 
        // store it in a private field/property, etc.)
    }
}

세션 사용시 자주 발생하는 오류

  • "Unable to resolve service for type 'Microsoft.Extensions.Caching.Distributed.IDistributedCache' while attempting to activate 'Microsoft.AspNetCore.Session.DistributedSessionStore'."

    대부분 이 오류는 최소 한 가지 이상의 IDistributedCache 구현 구성에 실패할 경우 발생합니다. 보다 자세한 정보는 Working with a Distributed Cache 문서와 In memory caching 문서를 참고하시기 바랍니다.

추가 자료