ASP.NET Web API와 HTTP 쿠키

등록일시: 2013-10-02 09:00,  수정일시: 2013-10-15 10:16
조회수: 9,187
이 문서는 ASP.NET Web API 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

본문에서는 Web API에서 HTTP 쿠키가 전송되고 수신되는 방식을 살펴봅니다.

HTTP 쿠키의 기본개념

이번 절에서는 HTTP 수준의 쿠키 구현 방식을 간단하게 살펴봅니다. 보다 자세한 정보는 RFC 6265를 참고하시기 바랍니다.

쿠키는 서버가 HTTP 응답의 일부로 전송하는 데이터 조각입니다. 클라이언트는 쿠키를 받아서 (선택적으로) 저장했다가, 그 다음 요청 시 이 쿠키를 다시 서버로 재전송합니다. 이런 과정들을 통해서 클라이언트와 서버가 서로 상태를 공유할 수 있습니다. 서버는 응답에 Set-Cookie 헤더를 포함시키는 방식으로 쿠키를 설정합니다. 쿠키는 이름-값 쌍과 선택적인 어트리뷰트들로 구성됩니다. 가령, 다음과 같은 구조를 갖고 있습니다:

Set-Cookie: session-id=1234567

그리고, 다음은 몇 가지 어트리뷰트들이 추가된 경우입니다:

Set-Cookie: session-id=1234567; max-age=86400; domain=example.com; path=/;

클라이언트에서 이 쿠키를 서버로 재전송하려면, 요청에 다음과 같은 Cookie 헤더를 포함시킵니다.

Cookie: session-id=1234567

HTTP 응답은 Set-Cookie 헤더를 여러 개 포함할 수 있습니다.

Set-Cookie: session-token=abcdef;
Set-Cookie: session-id=1234567;

반면, 클라이언트는 단일 Cookie 헤더를 사용해서 여러 개의 쿠키를 재전송합니다.

Cookie: session-id=1234567; session-token=abcdef;

쿠키의 범위와 존속 기간은 Set-Cookie 헤더에 뒤이어 지정되는 어트리뷰트로 제어가 가능합니다:

  • Domain: 클라이언트에서 어떤 도메인에 대해 해당 쿠키를 받아야 할지 알려줍니다. 가령, 이 값이 "example.com"인 경우라면 클라이언트는 example.com의 모든 서브 도메인에서 쿠키를 재전송합니다. 값이 지정되지 않으면 현재 서버가 기본값으로 사용됩니다.
  • Path: 쿠키를 도메인 하위의 특정 경로로만 제한합니다. 값이 지정되지 않으면 기본값으로 요청 URI 경로가 사용됩니다.
  • Expires: 쿠키 만료일자를 지정합니다. 만료일자에 도달하면 클라이언트가 쿠키를 삭제합니다.
  • Max-Age: 쿠키의 최대 나이(초)를 지정합니다. 최대 나이에 도달하면 클라이언트가 쿠키를 삭제합니다. (역주: IE의 구버전에서는 이 어트리뷰트가 동작하지 않는 것으로 알고 있습니다. 최신 버전에서는 테스트 해보지 못했습니다.)

만약, Expires 어트리뷰트와 Max-Age 어트리뷰트가 모두 설정되어 있다면 Max-Age 어트리뷰트가 더 높은 우선순위를 갖습니다. 반대로, 두 어트리뷰트가 모두 설정되어 있지 않으면 현재 세션이 종료될 때 클라이언트가 쿠키를 삭제합니다. (여기서 "세션"에 대한 정확한 개념은 사용자-에이전트에 의해서 정의됩니다.)

그렇지만, 클라이언트가 쿠키를 아예 무시해버릴 수도 있으므로 주의해야 합니다. 가령, 사용자가 개인 정보 보호를 위해서 쿠키를 비활성화 시켜버릴 수도 있습니다. 또는, 만료가 되기도 전에 클라이언트가 쿠키를 삭제해 버릴 수도 있고 쿠키 저장소의 개수를 제한할 수도 있습니다. 때로는 개인 정보 보호를 위해서 클라이언트가 현재 서버 이외의 다른 도메인의 "3사 쿠키"를 거부하는 경우도 많습니다. 간단하게 말해서, 근본적으로 서버에서 설정한 쿠키가 반드시 재전송 될 것이라고 단정지어서는 안된다는 말입니다.

Web API와 쿠키

쿠키를 HTTP 응답에 추가하려면 먼저 쿠키 자체를 의미하는 CookieHeaderValue 클래스의 인스턴스를 생성해야 합니다. 그런 다음, System.Net.Http.HttpResponseHeadersExtensions 클래스에 정의되어 있는 AddCookies 확장 메서드를 사용해서 쿠키를 추가합니다.

가령, 다음 코드는 컨트롤러에서 쿠키를 추가하는 예제입니다:

public HttpResponseMessage Get()
{
    var resp = new HttpResponseMessage();
    
    var cookie = new CookieHeaderValue("session-id", "12345");
    cookie.Expires = DateTimeOffset.Now.AddDays(1);
    cookie.Domain = Request.RequestUri.Host;
    cookie.Path = "/";
    
    resp.Headers.AddCookies(new CookieHeaderValue[] { cookie });
    return resp;
}

이 코드에서 AddCookies 메서드가 CookieHeaderValue 인스턴스의 배열을 전달받는다는 점에 유의하시기 바랍니다.

반대로 클라이언트 요청에서 쿠키를 추출하려면 GetCookies 메서드를 호출합니다:

string sessionId = "";
    
CookieHeaderValue cookie = Request.Headers.GetCookies("session-id").FirstOrDefault();
if (cookie != null)
{
    sessionId = cookie["session-id"].Value;
}

이 코드에서 CookieHeaderValue의 개체는 CookieState 인스턴스들의 컬렉션을 갖고 있습니다. 각각의 CookieState는 하나의 쿠키를 나타내며, 예제 코드에서 볼 수 있는 것처럼 인덱서 메서드에 이름을 지정해서 CookieState를 가져올 수 있습니다.

쿠키 데이터의 구조

많은 브라우저들이 전체 개수 및 도메인 당 개수 모두를 기준으로, 저장할 수 있는 쿠키를 최대량을 제한하고 있습니다. 따라서, 여러 개의 쿠키를 사용하는 대신 하나의 쿠키에 데이터를 구조적으로 설정하는 것이 유용할 수 있습니다.

노트: RFC 6265는 쿠키 데이터의 구조에 대해서는 정의하고 있지 않습니다.

CookieHeaderValue 클래스를 사용하면 이름-값 쌍의 목록을 쿠키 데이터로 전달할 수 있습니다. 이 때, 설정된 이름-값 쌍들은 URL-인코딩 된 폼 데이터의 형태로 Set-Cookie 헤더에 인코딩됩니다:

var resp = new HttpResponseMessage();
   
var nv = new NameValueCollection();
nv["sid"] = "12345";
nv["token"] = "abcdef";
nv["theme"] = "dark blue";
var cookie = new CookieHeaderValue("session", nv);
   
resp.Headers.AddCookies(new CookieHeaderValue[] { cookie });

이 코드는 다음과 같은 Set-Cookie 헤더를 만들어내게 됩니다:

Set-Cookie: session=sid=12345&token=abcdef&theme=dark+blue;

물론, CookieState 클래스도 요청값으로부터 쿠키의 서브-값들을 읽어올 수 있는 인덱서 메서드를 제공해줍니다:

string sessionId = "";
string sessionToken = "";
string theme = "";
   
CookieHeaderValue cookie = Request.Headers.GetCookies("session").FirstOrDefault();
if (cookie != null)
{
    CookieState cookieState = cookie["session"];
   
    sessionId = cookieState["sid"];
    sessionToken = cookieState["token"];
    theme = cookieState["theme"];
}

예제: 메시지 헨들러를 이용해서 쿠키를 설정하고 가져오기

지금까지는 Web API 컨트롤러에서 쿠키를 다루는 방법을 살펴봤습니다. 선택할 수 있는 한 가지 또 다른 방법은 메시지 핸들러(Message Handler)를 사용하는 것입니다. 메시지 핸들러는 파이프라인 상에서 컨트롤러보다 먼저 호출됩니다. 메시지 헨들러는 요청이 컨트롤러까지 도달하기 전에 먼저 쿠키를 읽거나, 컨트롤러가 응답을 생성한 후에 응답에 쿠키를 추가할 수도 있습니다.

다음 코드는 세션 ID를 생성해주는 메시지 핸들러 예제로, 세션 ID를 쿠키에 저장하고 있습니다. 헨들러는 먼저 요청을 검사해서 세션 쿠키가 존재하는지 여부를 확인한 다음, 쿠키가 존재하지 않으면 새로운 세션 ID를 생성합니다. 그와 반대로 이미 쿠키가 존재하는 경우에는 세션 ID를 HttpRequestMessage.Properties 프로퍼티 백에 저장합니다. 더불어, 세션 쿠키를 HTTP 응답에 추가하는 역할도 수행합니다.

노트: 이 예제는 클라이언트에서 전달된 세션 ID의 유효성을 검사하지 않는데, 사실상 서버에서 발급된 세션 ID 그대로이기 때문입니다. 그러므로, 이 코드를 있는 그대로 인증 용도로 사용해서는 안됩니다! 이 예제의 요점은 단지 HTTP 쿠키를 관리하는 방법을 살펴보는 것일 뿐입니다.
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
   
public class SessionIdHandler : DelegatingHandler
{
    static public string SessionIdToken = "session-id";
   
    async protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        string sessionId;
   
        // 요청에서 세션 ID를 가져오려고 시도하고, 없으면 새 ID를 생성한다.
        var cookie = request.Headers.GetCookies(SessionIdToken).FirstOrDefault();
        if (cookie == null)
        {
            sessionId = Guid.NewGuid().ToString();
        }
        else
        {
            sessionId = cookie[SessionIdToken].Value;
            try
            {
                Guid guid = Guid.Parse(sessionId);
            }
            catch (FormatException)
            {
                // 유효하지 않은 세션 ID. 새로운 ID를 생성한다.
                sessionId = Guid.NewGuid().ToString();
            }
        }
   
        // 세션 ID를 요청의 프로퍼티 백에 저장한다.
        request.Properties[SessionIdToken] = sessionId;
   
        // HTTP 요청을 계속 처리한다.
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
   
        // 응답 메시지에 세션 ID를 쿠키로 추가한다.
        response.Headers.AddCookies(new CookieHeaderValue[] {
            new CookieHeaderValue(SessionIdToken, sessionId)
        });
   
        return response;
    }
}

컨트롤러 내부에서는 HttpRequestMessage.Properties 프로퍼티 백을 통해서 세션 ID를 가져올 수 있습니다.

public HttpResponseMessage Get()
{
    string sessionId = Request.Properties[SessionIdHandler.SessionIdToken] as string;
   
    return new HttpResponseMessage()
    {
        Content = new StringContent("Your session ID = " + sessionId)
    };
}