Microsoft MVP성태의 닷넷 이야기
.NET Framework: 178. WCF - 사용자 정의 인증 구현 예제 [링크 복사], [링크+제목 복사],
조회: 32724
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
WCF 사용자 정의 인증 구현 예제


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

wcf 인증 문제
; https://www.sysnet.pe.kr/3/0/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 
; https://docs.microsoft.com/en-us/archive/blogs/pedram/wcf-authentication-custom-username-and-password-validator




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) 파일 만드는 방법
; https://www.sysnet.pe.kr/2/0/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입니다.)







[이 토픽에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/8/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2010-05-06 09시36분
[정용훈] 질문 드렸던 사람입니다. 많은도움되었습니다.^^
그런데 Message레벨 암호화가 되는 바람에 속도가 너무 느려졌습니다.
무암호화 일때는 겨우 원하는 만큼의 속도가 나왔습니다. 하지만 암호화 일때는 이게 최신 컴퓨터에서 돌아가는 통신 속도가 맞는지 의심스러울정도로 느려지는군요.
메세지가 발생하는 속도보다 처리하는 속도가 느려서 오류가 발생하는 문제가 생겼습니다.
UserNamePasswordValidator에 무암호화 방식은 없는지 궁금합니다.~~
[guest]
2010-05-08 01시24분
무암호화 방식이라 하면... 이미 ID/PW가 평문으로 넘어간다는 것인데, 이런 경우라면 별다른 표준이 정해져 있지는 않습니다. WS-* 표준을 구현한 WCF 문제를 벗어난 것입니다.

대신에 그런 경우에는 생각을 조금 바꾸시면 되지 않을까요? 예를 들어, 사용자 정의 헤더로 UserName, Password를 정의하고 넘겨준다거나... 하는 것과 별반 다르지 않을 것 같습니다
kevin25
2012-09-12 10시52분
[Foxpro87] 좋은글 잘 보았습니다. 실버라이트로 인증방법을 찾고 있었습니다.
첨부한 프로젝트를 실행했는데 오류가 납니다. - 다음 검색 조건을 사용하여 X.509 인증서를 찾을 수 없습니다. StoreName 'Root', StoreLocation 'LocalMachine', FindType 'FindBySubjectName', FindValue 'myserver'.
인터넷익스플로러 옵션>내용>인증서 에서 설치된것은 확인을 했습니다. (인증서관련은 처음이라 제대로 설치했는지 확실하지 않습니다.)

"자... 여기까지 했으면 빌드하고 다른 컴퓨터에서 실행시킵니다. (물론, 인증서는 그 컴퓨터에 등록되어 있어야 합니다.)"
위에 다른 컴퓨터에서 실행한다는 의미를 잘 모르겠습니다. 인증서가 필요없는 WCF 프로젝트는 서버/Client 동일 컴퓨터에서 동작하는 거 같더군요.
다른 컴퓨터라고 하면 서버프로젝트는 A컴퓨터에서 실버라이트는 Client프로젝트는 B컴퓨터에서 실행한다는 의미인가요? (초보적인 질문을 드려 죄송합니다. ^^)
[guest]
2012-09-12 01시19분
일단, 아래의 글을 먼저 읽어보신 다음에 다시 질문해 주세요. ^^

윈도우즈 인증서 서비스 이야기
; http://www.sysnet.pe.kr/2/0/353
정성태

1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13843정성태12/13/20244382오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20244534디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20244866오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20244439오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
13839정성태12/11/20244861오류 유형: 936. msbuild - error CS1617: Invalid option '12.0' for /langversion. Use '/langversion:?' to list supported values.
13838정성태12/4/20244595오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/20245058디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
13836정성태12/3/20244625디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
13835정성태12/2/20245071오류 유형: 934. Azure - rm: cannot remove '...': Directory not empty
13834정성태11/29/20245287Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우) [1]파일 다운로드1
13833정성태11/29/20244979개발 환경 구성: 737. Azure Web App에서 Scale-out으로 늘어난 리눅스 인스턴스에 SSH 접속하는 방법
13832정성태11/27/20244911Windows: 274. Windows 7부터 도입한 conhost.exe
13831정성태11/27/20244382Linux: 111. eBPF - BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_RINGBUF에 대한 다양한 용어들
13830정성태11/25/20245185개발 환경 구성: 736. 파이썬 웹 앱을 Azure App Service에 배포하기
13829정성태11/25/20245164스크립트: 67. 파이썬 - Windows 버전에서 함께 설치되는 py.exe
13828정성태11/25/20244441개발 환경 구성: 735. Azure - 압축 파일을 이용한 web app 배포 시 디렉터리 구분이 안 되는 문제파일 다운로드1
13827정성태11/25/20245091Windows: 273. Windows 환경의 파일 압축 방법 (tar, Compress-Archive)
13826정성태11/21/20245320닷넷: 2313. C# - (비밀번호 등의) Console로부터 입력받을 때 문자열 출력 숨기기(echo 끄기)파일 다운로드1
13825정성태11/21/20245668Linux: 110. eBPF / bpf2go - BPF_RINGBUF_OUTPUT / BPF_MAP_TYPE_RINGBUF 사용법
13824정성태11/20/20244751Linux: 109. eBPF / bpf2go - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법
13823정성태11/20/20245290개발 환경 구성: 734. Ubuntu에 docker, kubernetes (k3s) 설치
13822정성태11/20/20245155개발 환경 구성: 733. Windbg - VirtualBox VM의 커널 디버거 연결 시 COM 포트가 없는 경우
13821정성태11/18/20245079Linux: 108. Linux와 Windows의 프로세스/스레드 ID 관리 방식
13820정성태11/18/20245250VS.NET IDE: 195. Visual C++ - C# 프로젝트처럼 CopyToOutputDirectory 항목을 추가하는 방법
13819정성태11/15/20244490Linux: 107. eBPF - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성
13818정성태11/15/20245291Windows: 272. Windows 11 24H2 - sudo 추가
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...