블로그 이미지
Sunny's

calendar

1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31

Notice

2011. 5. 12. 13:04 ASP.NET

출처 : http://www.sysnet.pe.kr/Default.aspx?mode=2&sub=0&detail=1&wid=864
WCF 사용자 정의 인증 구현 예제


사실 ID/Password 기반으로 WCF를 구현하려 할 때, 윈도우 인증이나 인증서 기반으로 구현하는 것은 일반적인 업무 환경에서 사용하기에는 약간 부적합한 면은 있습니다. 게다가 사용자 정의 인증을 한다 해도 반드시 평문 전달을 막는 안전 장치가 있어야 하는데요. 이 과정에서도 역시 인증서가 꼭 끼게 되는데, 이를 설정하는 WCF 옵션이 하도 다양하다 보니... 관련해서 오해를 하시는 분들이 가끔 있습니다. 마침, 아래의 질문을 하신 분도 있으니... 이 참에 사용자 정의 인증 예제를 다뤄보도록 하겠습니다.

wcf 인증 문제
; http://www.sysnet.pe.kr/Default.aspx?mode=3&sub=0&detail=1&wid=879

다행히 "사용자 정의 인증" 구현에 대해서 웹 상에 찾아보면 자료가 많이 있고, 저도 아래의 글을 참조해서 실습을 할텐데 총 4개의 프로젝트로 구성합니다.

Silverlight 3: Securing your WCF service with a custom username / password authentication mechanism 
; http://blogs.infosupport.com/blogs/alexb/archive/2009/10/02/silverlight-3-securing-your-wcf-service-with-a-custom-username-and-password-authentication-mechanism.aspx

WCF Authentication: Custom Username and Password Validator 
; http://blogs.msdn.com/pedram/archive/2007/10/05/wcf-authentication-custom-username-and-password-validator.aspx




1. WcfLibrary - 인터페이스 정의 프로젝트

라이브러리 유형의 프로젝트를 하나 만들고 그 안에 WCF 서비스 인터페이스를 정의합니다.

// ======= IHelloWorld.cs =======

namespace WcfLibrary
{
    [ServiceContract]
    public interface IHelloWorld
    {
        [OperationContract]  
        string SayHello();    
    }
}

2. UserNamePasswordAuth - 사용자 정의 인증 모듈 프로젝트


WCF 클라이언트 측에서 전송되는 ID/PW를 확인하는 모듈을 구현합니다.

// ======= DatabaseBasedValidator.cs =======

namespace UserNamePasswordAuth
{
    public class DatabaseBasedValidator : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if (userName == "test" && password == "test")
            {
                return;
            }

            throw new FaultException("Test account can be authenticated ONLY.");
        }
    }
}

재미있는 것은, Validate 메서드 안에서 예외를 발생시키면 인증이 실패하는 것이고 예외 없이 반환하면 인증이 성공한 것입니다. (개인적으로 가장 궁금한 것이... 왜 굳이 예외를 사용해야 했었느냐 하는 것입니다. true/false 반환도 나쁘진 않았을 텐데.)

위에서는 간단하게 구현하느라 하드 코딩을 했지만, 원래 Validate 메서드에는 DB 에 저장된 ID/PW 를 확인하는 작업을 하는 것이 보통이죠.

3. WcfServer - WCF 서비스 호스트


서비스 호스트 프로젝트에서 하는 일이 가장 많습니다. ^^
테스트를 용이하게 하기 위해 콘솔 유형의 프로젝트를 생성하고, 1번과 2번에서 만든 프로젝트를 참조한 후 다음과 같이 IHelloWorld 의 구현 클래스를 만들어 줍니다.

// ======= HelloService.cs =======

namespace WcfServer
{
    public class HelloService : IHelloWorld
    {
        public string SayHello()
        {
            return "Hello: " + OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name;
        }
    }
}

이어서, HelloService 타입을 호스팅합니다.

namespace WcfServer
{
    class Program
    {
        static void Main(string[] args)
        {
            using (ServiceHost serviceHost = new ServiceHost(typeof(HelloService)))
            {
                serviceHost.Open();

                Console.WriteLine("Press any key to exit...");
                Console.ReadLine();
            }
        }
    }
}

binding 설정과 함께 이전에 만들어 둔 UserNamePasswordAuth 모듈을 app.config 파일을 통해서 연결해 줍니다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service behaviorConfiguration="helloServiceBehavior" 
               name="WcfServer.HelloService">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:9000"/>
            <add baseAddress="net.tcp://localhost:9001"/>
          </baseAddresses> 
        </host>  
        <endpoint address="myservice" binding="netTcpBinding" 
                  bindingConfiguration="netTcpBindingConf" 
                  contract="WcfLibrary.IHelloWorld">
          </endpoint>  
        <endpoint address="mex" binding="mexHttpBinding" 
                  contract="IMetadataExchange"/>  
      </service>  
    </services>  
    
    <bindings>
      <netTcpBinding>
        <binding name="netTcpBindingConf">
            <security mode="Message">
              <message clientCredentialType="UserName"/> 
            </security>  
        </binding>  
      </netTcpBinding>  
    </bindings>
    
    <behaviors>
      <serviceBehaviors>
        <behavior name="helloServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/> 
            <serviceDebug includeExceptionDetailInFaults="true"/> 
            <serviceCredentials>
              <userNameAuthentication userNamePasswordValidationMode="Custom"  
                                          customUserNamePasswordValidatorType="UserNamePasswordAuth.DatabaseBasedValidator, UserNamePasswordAuth"/>
              <serviceCertificate
                     findValue="myserver"
                     x509FindType="FindBySubjectName"
                     storeLocation="LocalMachine"
                     storeName="Root" />

            </serviceCredentials>  
        </behavior>  
      </serviceBehaviors>  
    </behaviors> 
  </system.serviceModel>  
  
</configuration>

간단히 특이한 부분만을 살펴보겠습니다. 우선 <security /> 노드에 mode="Message"라고 하면 WCF 메시지가 암호화되어 전송됩니다. 그리고 <message clientCredentialType="UserName"/> 구문을 넣어주어야 WCF 클라이언트로부터 ID/PW 를 입력받는 인증방식이 선택되는 것입니다.

이전에 만들었던 UserNamePasswordAuth.DatabaseBasedValidator 타입을 다음과 같이 연결해 준 것이 눈에 띕니다.

<userNameAuthentication userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="UserNamePasswordAuth.DatabaseBasedValidator, UserNamePasswordAuth"/>



마지막으로 설정하기 곤란한 것이... 인증서 부분인데요.

<serviceCertificate
        findValue="myserver"
        x509FindType="FindBySubjectName"
        storeLocation="LocalMachine"
        storeName="Root" />

위의 정의가 없으면 무조건 예외가 발생합니다. 왜냐하면 클라이언트로부터 전달되는 ID/PW 가 평문으로 오기 때문에 심각한 보안 결함이라 여기고 동작을 하지 않는 것입니다. (아니면 HTTPS 와 같은 트랜스포트 레벨의 보안이 되는 바인딩을 지정해야 합니다.)

여기서 인증서까지 설명하면 너무 길어지기 때문에 넘어가겠습니다. 대신 다음의 글을 참고하시면 됩니다.

인증서 관련(CER, PVK, SPC, PFX) 파일 만드는 방법
; http://www.sysnet.pe.kr/Default.aspx?mode=2&sub=0&detail=1&wid=863

그렇게 해서 설치한 인증서를 <serviceCertificate /> 노드에 적절하게 설정해 주시면 됩니다. 또는 인증서 자체를 pfx로부터 읽어들여서 지정하는 방법 등 다양하게 있으니 그 부분은 나중에 기회되면 또 설명드리겠습니다. (물론, 웹에 자료는 널려 있습니다.)

자... 여기까지 했으면 빌드하고 다른 컴퓨터에서 실행시킵니다. (물론, 인증서는 그 컴퓨터에 등록되어 있어야 합니다.)

4. WcfClient - WCF 클라이언트


마지막으로 문제가 되는 WCF 클라이언트입니다. wcf 인증 문제 를 물어보신 분은, 바로 이 클라이언트를 구동할 때 서버 측이 사용한 인증서가 클라이언트의 루트 인증서에 등록된 기관으로부터 서명받은 것이어야 한다는 내용인데요. 한번 살펴보겠습니다. 코드를 간단하게 만들기 위해 프록시 클래스 생성은 하지 않고 ChannelFactory를 이용해서 곧바로 사용해 보겠습니다.

namespace WcfClient
{
    class Program
    {
        static void Main(string[] args)
        {
            using (ChannelFactory<IHelloWorld> factory =
                new ChannelFactory<IHelloWorld>("TcpNetConf"))
            {
                factory.Credentials.UserName.UserName = "test";
                factory.Credentials.UserName.Password = "test";

                IHelloWorld svc = factory.CreateChannel();
                using (svc as IDisposable)
                {
                    Console.WriteLine(svc.SayHello());
                }
            }           
        }
    }
}

중요한 것은 "TcpNetConf"와 연결되는 app.config 설정이죠.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint name="TcpNetConf"
        address="net.tcp://myserver:9001/myservice"
        binding="netTcpBinding"
        bindingConfiguration="netTcpBindingConf"
        behaviorConfiguration="netTcpBehavior"
        contract="WcfLibrary.IHelloWorld">

      </endpoint>
    </client>

    <bindings>
      <netTcpBinding>
        <binding name="netTcpBindingConf">
          <security mode="Message" >
            <message clientCredentialType="UserName"/>
          </security>
        </binding>
      </netTcpBinding>
    </bindings>

    <behaviors>
      <endpointBehaviors>
        <behavior name="netTcpBehavior">
          <clientCredentials>
            <serviceCertificate>
              <authentication certificateValidationMode="None" />
            </serviceCertificate>
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

좀 특이한 것이 보이나요? ^^

우선 당연히 클라이언트 측도 UserName 인증 방식을 사용하도록 <security /> 노드를 구성해야 하고... 아하~~~ 문제는 certificateValidationMode 를 None으로 지정해 주면 되는 것이었군요. ^^

<authentication certificateValidationMode="None" />

이렇게 해주면 클라이언트에 아무런 인증서를 설치하지 않아도 됩니다. 끝~~~~!




첨부한 프로젝트는 제가 사용한 예제 프로젝트입니다. (제공되는 mycert.pfx 의 암호는 1000 입니다.)
posted by Sunny's