Visual Studio Camp #1 세미나를 무사히 마친 지 일주일이 지났습니다. 그 날 날씨가 많이 안좋았음에도 불구하고 많은 분들이 오셔서 너무 고마웠습니다. 나름 준비도 좀 많이 하고, 더 많은 걸 보여드리고 싶었는데, 정해진 시간의 압박으로 그렇게 하지 못해서 아쉬웠습니다. 더구나, 마지막엔 제대로 된 결과도 못 보여드리고,, ㅡㅠ
어찌됐든, 앞으로도 많은 정보를 알려 드릴 수 있도록 열심히 해보겠습니다. 많은 응원 부탁드려요~ ^^

지난 아티클에 이어서, 이번에는 전송 계층에서의 메세지 인증을 할 수 있는 서비스를 만들어 보려 합니다.

ASP.NET 웹 사이트에서 인증을 하는 방법에는 폼 인증과 윈도우즈 인증이 있습니다. 하지만, 보통 웹 어플리케이션을 구축할 때 윈도우즈 인증 보다는 폼 인증을 많이 쓰죠~ (개인적으로, 아직 경험이 미천하여, 윈도우 인증은 적용을 해본 적이 거의 없습니다. ^^;;) 사용자에 대한 정보를 데이터베이스에 따로 저장하고, 그 값을 가져와 인증을 해주는 그러한 방법,,
 
그래서, 이번 아티클에서도 데이터베이스에 사용자 정보를 두고, 이를 이용하여 인증을 할 수 있는 서비스를 만들어보겠습니다.

지난 아티클에서 만들었던 솔루션을 그대로 이용해서, SSL을 이용한 보안을 그대로 사용하려 합니다. 그리고, 여기에 사용자 이름과 패스워드를 이용한 인증을 처리하는 부분을 추가해보도록 하겠습니다. 그래서, 먼저 지난 아티클에서 만들었던 솔루션을 불러오고, 서비스를 다음과 같이 정의합니다.

[ServiceContract]

public interface IService

{

    [OperationContract]

    List<Product> GetAllProducts();

}

 

[DataContract]

public class Product

{

    [DataMember]

    public int Id { get; set; }

 

    [DataMember]

    public string Name { get; set; }

 

    [DataMember]

    public double Price { get; set; }

}


public class Service : IService

{

    public List<Product> GetAllProducts()

    {

        List<Product> products = null;

           

        // (SecurityException)

        if (OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.IsAuthenticated == false)

        {
            // SecurityException 클래스는 System.Security 네임스페이스에 정의 되어 있다.

            throw new SecurityException();

        }

        else

        {

            products = new List<Product>()

            {

                new Product { Id = 1, Name = "Visual Studio 2010", Price = 223.00 },

                new Product { Id = 2, Name = "Expression Blend 4", Price = 133.00 },

                new Product { Id = 3, Name = "Team Foundation Server 2010", Price = 253.00 }

            };

 

            return products;

        }

    }

}


GetAllProducts 메서드에서 인증이 되지 않았을 경우 예외를 발생하는 부분이 있는데, 이 부분만 조금 주의해서 봐주시면 될 것 같습니다. 다른 부분은 지금까지 해온 여타 서비스와 다를게 없죠,,

이제 인증을 처리하는 코드를 작성하려 합니다. 그 전에 인증에 필요한 사용자 정보를 담아둘 데이터베이스가 필요하니깐, 이를 만들어둡니다. 저는 다음과 같은 간단한 Member 테이블을 만들어 보았습니다.


그리고, AuthenticationHelper 라는 이름으로 새로운 클래스를 하나 추가하고, 다음과 같은 코드를 작성합니다.


public class AuthenticationHelper : UserNamePasswordValidator

{

    SqlConnection conn;

    SqlCommand cmd;

    SqlDataReader reader;

 

    public override void Validate(string userName, string password)

    {

        if (userName == null || password == null)

        {

            throw new Exception("User Name or Password cannot be null");

        }

 

        if (!this.CheckIfUserNameExist(userName))

        {

            throw new Exception("Sorry! This User is Not Present");

        }

 

        if (!this.AuthenticateUser(userName, password))

        {

            throw new Exception("Invalid User Name or Password");

        }

    }

 

    private bool CheckIfUserNameExist(string userName)

    {

        bool exists = false;

        this.conn = new SqlConnection
                         ("Server=.\\SQLEXPRESS;Database=Temp;User Id=sa;Password=1111");

        this.cmd = new SqlCommand();

        this.cmd.CommandText = "SELECT UserName FROM Member WHERE UserName=@UserName";

        this.cmd.Connection = this.conn;

        this.cmd.Parameters.AddWithValue("@UserName", userName);

 

        try

        {

            this.conn.Open();

            this.reader = this.cmd.ExecuteReader();

            DataTable dtUser = new DataTable();

            dtUser.Load(this.reader);

 

            int count = dtUser.Rows.Count;

            if (count != 0)

                exists = true;

        }

        catch (SqlException ex)

        {

            throw ex;

        }

        finally

        {

            this.conn.Close();

        }

 

        return exists;

    }

 

    private bool AuthenticateUser(string userName, string password)

    {

        bool valid = false;

 

        this.conn = this.conn = new SqlConnection
                         ("Server=.\\SQLEXPRESS;Database=Temp;User Id=sa;Password=1111");

        this.cmd = new SqlCommand();

        this.cmd.CommandText = "SELECT Password FROM Member WHERE UserName=@UserName";

        this.cmd.Connection = this.conn;

        this.cmd.Parameters.AddWithValue("@UserName", userName.Trim());

 

        try

        {

            this.conn.Open();

            this.reader = this.cmd.ExecuteReader();

               

            reader.Read();

            if (reader["Password"].ToString() == password.Trim())

                valid = true;

        }

        catch (SqlException ex)

        {

            throw ex;

        }

        finally

        {

            this.conn.Close();

        }

 

        return valid;

    }

}

이번 코드는 조금 길군요 ^^;;
하지만, 코드를 조금 살펴보시면 알겠지만 그렇게 어려운 코드는 아닙니다. 코드가 길다고 어려워 할 필요 없다구요~ ㅎ

주목해야 할 점은 AuthenticationHelper 클래스가 UserNamePasswordValidator 클래스를 상속하고 있다는 것인데, UserNamePasswordValidator 클래스는 WCF 서비스에서 사용자 지정 사용자 이름 및 암호 유효성 검사기를 만들기 위한 클래스입니다. (사용자 지정 사용자 이름 및 암호 유효성 검사기 사용 참고)
한마디로, 윈도우즈 인증이 아닌 사용자가 정의한 인증 방법을 WCF 서비스에 적용하기 위한 유효성 검사기를 만들기 위한 클래스라고 이해하시면 될 것 같습니다.

유효성 검사기를 만드는 방법은 아주 간단한데, UserNamePasswordValidator 클래스에 정의 된 Validate 메서드를 재 정의(overriding) 해주면 됩니다.

이제, 거의 서비스를 다 만든 것 같습니다. 마지막으로 web.config를 수정하여, 사용자 지정 인증 방법을 쓸 수 있도록 해주는 일이 남았습니다. 그리고, 지난번 아티클에서 만들었던 인증서를 사용하도록 해주는 태그도 필요합니다.

web.config 파일을 다음과 같이 수정해 보도록 하겠습니다.

<system.serviceModel>

  <services>

    <service name="SSLService.Service" behaviorConfiguration="MyBehavior">

      <endpoint address="" binding="basicHttpBinding" bindingConfiguration="MyBind"

        contract="SSLService.IService">

        <identity>

          <dns value="localhost"/>

        </identity>

      </endpoint>

      <endpoint address="mex" binding="mexHttpsBinding" contract="IMetadataExchange" />

      <host>

        <baseAddresses>

          <add baseAddress="http://localhost:4949/Service1.svc" />

          <add baseAddress="https://localhost:4948/Service1.svc" />

        </baseAddresses>

      </host>

    </service>

  </services>

 

  <bindings>

    <basicHttpBinding>

      <binding name="MyBind">
        <!-- 메세지 자격증명을 위한 설정 -->

        <security mode="TransportWithMessageCredential">

          <message clientCredentialType="UserName"/>

        </security>

      </binding>

    </basicHttpBinding>

  </bindings>

  <behaviors>

    <serviceBehaviors>

      <behavior name="MyBehavior">

        <serviceCredentials>
          <!-- 메시지 보안 모드를 사용하는 클라이언트에 대한 서비스를
                인증하는 데 사용할 X.509 인증서를 지정합니다-->

          <serviceCertificate storeName="My" storeLocation="LocalMachine"
                                      x509FindType=
"FindBySubjectName"

                                      findValue="Dreamer"/>
          <!-- 사용자 인증을 할 때 사용할 component를 정의합니다. -->

          <userNameAuthentication

            userNamePasswordValidationMode="Custom"

            customUserNamePasswordValidatorType="SSLService.AuthenticationHelper,SSLService"/>

        </serviceCredentials>

        <serviceMetadata httpGetEnabled="true" />

        <serviceDebug includeExceptionDetailInFaults="false" />

      </behavior>

    </serviceBehaviors>

  </behaviors>

  <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />

</system.serviceModel>


web.config 파일도 뭔가 내용이 많습니다. 역시 보안 설정은 어려워요,, ^^;;

하지만, 자세히 보면 익숙하지 않은 태그는 그리 많지 않습니다. 이 태그들에는 대충의 주석을 달아놨으니 이해하기는 어렵지 않을 것 같구요~

전체적으로 설명을 붙이자면, 서비스에 적용할 Behavior와 Binding에 대한 설정이 필요하여 각각 "MyBehavior", "MyBind" 라는 이름으로 태그를 추가하였고, 그 안에 필요한 태그들을 추가하였습니다.

"serviceCertificate" 태그는 인증 시 사용할 인증서를 지정하는데 지난 아티클에서 만든 인증서를 사용하면 됩니다. 이때, "findValue" 속성에 들어갈 값은 인증서의 발급자에 들어있는 값을 입력하여야 합니다.
그리고, "userNameAuthentication" 태그는 사용자 인증 시 사용할 component를 정의하는데 "customUserNamePasswordValidatorType" 속성에는 UserNamePassowrdValidator 클래스를 상속받아 구현 된 클래스 명을 입력해주면 됩니다.

서비스 구현은 모두 끝났습니다. 이제 콘솔 어플리케이션을 이용해 이 서비스를 사용해보도록 하겠습니다.
클라이언트 구현은 이 전에 구현했던 다른 클라이언트들과 다를바가 없습니다. 단지, 서비스 인증을 위해 사용자 이름과 패스워드를 입력해줘야 하는 부분만 추가해주면 됩니다.

콘솔 어플리케이션 프로젝트를 추가한 후에 서비스를 추가하고(이때, https 로 시작되는 url을 이용하여 서비스를 추가합니다.), 다음과 같이 Main 메소드를 구현합니다.

static void Main(string[] args)

{

    ServiceClient proxy = new ServiceClient();

    // 패스 .

    proxy.ClientCredentials.UserName.UserName = "ruaa";

    proxy.ClientCredentials.UserName.Password = "P@ssw0rd";

 

    Product[] products = proxy.GetAllProducts();

 

    foreach (Product p in products)

    {

        Console.WriteLine("ID : {0}", p.Id);

        Console.WriteLine("Name : {0}", p.Name);

        Console.WriteLine("Price : {0:f}\n", p.Price);

    }

}


보이시죠? 어떻게 인증을 위한 사용자 이름과 패스워드를 입력하는지,, ㅎ

당연히 여기에 입력되는 사용자에 대한 데이터는 앞에서 만들었던 Member 테이블에 존재해야 합니다.

이렇게 하면, 다음과 같은 결과화면을 확인할 수 있습니다. ^^


네~ 이것으로 이번 포스팅도 끝이 났습니다.
긴 글 읽으시느라 모두들 수고하셨고, 다음 포스팅때 뵙겠습니다. 감사합니다~ ^^