세션 저장소 커스터마이징

지금까지 살펴본 바 세션 정보를 memcached를 이용하여 세션 정보를 성능과 관리, 그리고 확장가능성 측면에서 만족할 만한 솔루션이다.

여기에서 좀 더 나아가 쿠키 등을 이용하여 서브 도메인(Sub Domain)의 웹 응용 프로그램에 인증을 하거나 브라우저를 닫고 새로운 브라우저로 재접속 한 경우 기존 세션을 유지할 수 있도록 기능을 개선할 수 도 있다.

과거에는 SSO(Single-Sign-On)을 구현하기 위해 쿠키로 서브 도메인을 인증하는 경우 domain 에 의해 쿠키가 공유가 가능하다는 점을 이용하여 구현하기도 했다. 물론, SSO 솔루션들이 많이 있었지만, 수천 수만명의 사용자가 관리 대상이 아니라면 굳이 비싼 SSO 솔루션을 쓸 필요는 없었다.

세션 저장 방법 커스터마이징

ASP.NET MSSQL Session Table 또는 Windows Azure SQL Session Table은 다음과 같이 정의된다. (MSDN에 정의된 테이블 참조 1)

CREATE TABLE [ASPState].dbo.ASPStateTempSessions (
    SessionId         nvarchar(88)  NOT NULL PRIMARY KEY,
    Created          datetime       NOT NULL DEFAULT GETUTCDATE(),
    Expires          datetime       NOT NULL,
    LockDate            datetime        NOT NULL,
    LockDateLocal     datetime      NOT NULL,
    LockCookie       int             NOT NULL,
    Timeout          int             NOT NULL,
    Locked           bit             NOT NULL,
    SessionItemShort    VARBINARY(7000) NULL,
    SessionItemLong  image        NULL,
    Flags             int            NOT NULL DEFAULT 0,
)   

CREATE NONCLUSTERED INDEX Index_Expires ON [ASPState].dbo.ASPStateTempSessions(Expires)  

CREATE TABLE [ASPState].dbo.ASPStateTempApplications (
    AppId             int            NOT NULL PRIMARY KEY,
    AppName          char(280)    NOT NULL,
)   

CREATE NONCLUSTERED INDEX Index_AppName ON [ASPState].dbo.ASPStateTempApplications(AppName)

이 두 테이블에서 가장 중요한 것은 다음의 두 개의 Private Key가 되는 필드이다.

  • AppId

    웹 응용 프로그램을 구분하는 고유 Id 값이다. IIS는 여러 개의 웹 응용 프로그램을 호스팅할 수 있는데, 이 웹 응용 프로그램을 구분하는 값으로 사용된다.

  • SessionId

    웹 응용 프로그램 내에 세션 키로 사용되는 값이다. 일반적으로 이 값은 해쉬된 값으로 중복되지 않는다.

따라서 여러 웹 응용 프로그램에서 같은 SessionId를 사용할 수 있다면 인증에 대한 SSO(Single-Sign-On)은 구현되는 것과 마찬가지다. 그러므로 위의 테이블 중 dbo.ASPStateTempApplications 테이블은 없어져도 된다.

만약 MS SQL을 사용하여 세션 저장소로 이용하는 경우 SQL Session Provider가 사용하는 Stored Procedure의 Where 절에 AppId를 빼기만 하면 된다.

필자는 SQL Server가 아닌 memcached를 이용하므로 위의 구성은 굳이 생략이 가능하다.

MemCached Session Store Provider 커스터마이징

ASP.NET은 클라이언트(웹 브라우저를 사용하는 사용자)를 구분하기 위해 해시된 세션 키 값을 사용한다고 했다. 이 해시된 값은 없어도 되지만, 웹 응용 프로그램 내부적으로 세션을 사용한다면 반드시 필요한 키 값이다.

클라이언트에게는 서버 내부적으로 사용하는 해시된 키 값을 쿠키에 저장하고, 서버로 요청이 오는 경우 이 쿠키 값의 해시된 세션 키 값을 이용하여 세션 데이터를 조회하게 된다. 만약, 이 해시된 세션 키 값이 없다면 새로운 세션으로 인식한다.

SSO를 구현하기 위해서 신원이 확인된 사용자마다 완전히 유일한(Unique)한 키 값이 필요하다. 예를 들어, 이 값은 사용자의 고유 번호가 될 수 있고, 사용자 아이디 또는 이메일과 같은 유일한 값이 되어야 한다. 세션 키는 매번 변할 수 있는 값이기 때문에 유일한 값이긴 하나 사용자마다 변하지 않는 유일한 값이 될 수 없다.

이를 구현하는 방법은 매우 간단하다. SessionStateStoreProviderBase 추상 클래스를 구현할 때 Id 값을 고의로 변경하면 된다.

public sealed class MemcachedSessionStateStore : SessionStateStoreProviderBase
{
   void Command(Action action)         
   { 
        var pool = SockIOPool.GetInstance(); 

        // 필자의 원격 memcached IPs     
        pool.SetServers(new string[] { "192.168.0.23:11211", "192.168.0.23:11211", "192.168.0.23:11211" }); 
        pool.InitConnections      = 3;
        pool.MinConnections       = 3;
        pool.MaxConnections       = 5;
        pool.SocketConnectTimeout = 1000;
        pool.SocketTimeout        = 3000;
        pool.MaintenanceSleep     = 30;
        pool.Failover             = true;
        pool.Nagle                = false;
        pool.Initialize();

        action();
   }

   public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem)
    {
        try
        {
            if (context.User.Identity.IsAuthenticated)
            {
                id = context.User.Identity.Name;
            }

            var data = new SessionData()
                {
                    Id      = id,
                    LockAge = TimeSpan.FromMinutes(10),
                    LockId  = id,
                    Exfires =  DateTime.Now.AddMinutes(10)
                }.ToBinaryBytes().ToBase64();

            Command(() => new MemcachedClient().Set(id, data));

        }
        catch (Exception e)
        {
            // 생략...
        }
        finally
        {
        }
    }

    public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, 
                                                                                    out TimeSpan lockAge, 
                                                                                    out object lockId,
                                                                                    out SessionStateActions actionFlags)
    {
        return GetSessionStoreItem(false, context, id, out locked, out lockAge, out lockId, out actionFlags);
    }

    public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked,
                                                                                            out TimeSpan lockAge,
                                                                                            out object lockId,
                                                                                            out SessionStateActions actionFlags)
    {
        return GetSessionStoreItem(true, context, id, out locked, out lockAge, out lockId, out actionFlags);
    }


    public override void CreateUninitializedItem(HttpContext context, string id, int timeout)
    {
       if (context.User.Identity.IsAuthenticated)
        {
            id = context.User.Identity.Name;
        }

        var data = new SessionData()
        {
            Id = id,
            LockAge = TimeSpan.FromMinutes(10),
            LockId = id,
            Exfires = DateTime.Now.AddMinutes(10)
        }.ToBinaryBytes().ToBase64();

        Command(() => new MemcachedClient().Set(id, data));
    }

    public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
    {
        return new SessionStateStoreData(new SessionStateItemCollection(), SessionStateUtility.GetSessionStaticObjects(context), (int)timeout);
    }

    private string Serialize(SessionStateItemCollection items)
    {
        var ms = new MemoryStream();
        var writer = new BinaryWriter(ms);

        if (items != null)
            items.Serialize(writer);

        writer.Close();

        return Convert.ToBase64String(ms.ToArray());
    }


    private SessionStateStoreData Deserialize(HttpContext context, string serializedItems, int timeout)
    {
        var ms = new MemoryStream(Convert.FromBase64String(serializedItems));
        var sessionItems = new SessionStateItemCollection();

        if (ms.Length > 0)
        {
            var reader = new BinaryReader(ms);
            sessionItems = SessionStateItemCollection.Deserialize(reader);
        }

        return new SessionStateStoreData(sessionItems, SessionStateUtility.GetSessionStaticObjects(context), timeout);
    }  

// 이하 생략...  
// 이하 생략...  
// 이하 생략...

}  

인증된 사용자와 인증되지 않은 사용자의 세션 데이터

필자는 memcached의 사용자 세션 키를 다음과 같이 정의하여 구현하였다.

  1. 인증되지 않은 사용자는 해쉬된 세션 키를 사용한다.
  2. 인증된 사용자는 인증 정보를 세션 키로 사용한다. (UserName 정보)

모든 클라이언트의 웹 브라우저 쿠키에 ASP.NET 세션 키가 ASP.NET_SessionId 쿠키 값으로 저장된다. 이 값은 ASP.NET이 생성한 해쉬된 값이므로 언제든지 변할 수 있다가도, 사용자가 웹 응용프로그램에서 인증을 하게 되면 해쉬된 세션 키를 사용하지 않고 사용자 계정으로 세션 키 값을 대체하게 된다.

이 코드가 위의 코드 중에 다음과 같이 구현한 부분이다.

if (context.User.Identity.IsAuthenticated)
{
    id = context.User.Identity.Name;
}

그럼 현재까지 구현된 부분으로 다음과 같은 시나리오로 테스트를 하고 결과를 보자.

  1. 익명으로 웹 사이트 접속
  2. 익명 사용자의 세션 키 값을 memcached 에서 조회 (해쉬된 세션 키 rlvh3y4edt1nyzrt42sdgnvu)
  3. 웹 사이트 로그인 (로그인 사용자 계정 powerumc)

  4. 인증된 사용자의 계정으로 memcached 에서 조회

그럼 분산된 웹 응용 프로그램이나 다른 도메인의 웹 응용 프로그램 간에 서로 인증을 해보자. 폼 인증을 사용한 웹 응용 프로그램이므로 다른 웹 응용 프로그램에 폼 인증을 시킬 수 있는 방법만 있으면 된다. 웹 응용 프로그램 간에 토큰 값을 넘길 수 도 있고, 또는 요즘 유행하는 다른 인증 매커니즘을 이용할 수 도 있다.

어쨌든 서로 다른 웹 응용 프로그램이 하나의 memcached 세션 서버에 연결이 가능하다면 신원이 인증된, 위에’서 테스트 한 사용자 계정 ‘powerumc’는 어느 웹 응용 프로그램에서 세션을 조회하더라도 존재하게 된다.

사용자는 웹 브라우저를 완전히 닫거나 운영체제를 완전히 재시작하였다고 하더라도 세션이 만료되는 통상적인 시간인 약 20분 이전에 로그인만 한다면 이전에 저장된 세션 데이터를 가져와 마지막의 최신 상태를 유지할 수 있게 된다.

결론

지금까지 memcached를 이용하여 Session State Store Provider를 만들고 이를 활용하는 방법을 알아보았다. 분산된 세션 저장소를 사용해야 하는 경우 memcached를 이용하면 신뢰된 성능을 보장받을 수 있고, 세션 데이터를 유지하는 방법을 변형하여 좀 더 보안을 높이거나 유연하게 상호운용을 가능하도록 할 수도 있다.

  • 분산된 세션 저장소를 확장
  • memcached를 이용하여 신뢰할 수 있는 성능 발휘
  • 세션의 보안을 강화하거나 격리시킬 수 있는 방법이 가능
  • 세션의 상호운용성을 높여 서로 다른 도메인간에 인증 가능
  • 서로 다른 도메인간에, 서로 다른 플랫폼 간에 세션 데이터 공유 가능

이 외에 여러 가지 기법들을 활용하여 더 많은 것들을 가능할 수 있다. 세션의 활용이 웹 개발 플랫폼에서 그만큼 중요하고 보안과 직결되는 요소인 만큼 이 기회에 세션에 대한 내용을 모두 정복해 보기 바란다.

  1. MSDN에 정의된 테이블 참조 http://msdn.microsoft.com/en-us/library/aa478952.aspx


신고
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by 땡초 POWERUMC

댓글을 달아 주세요