Microsoft MVP성태의 닷넷 이야기
.NET Framework: 178. WCF - 사용자 정의 인증 구현 예제 [링크 복사], [링크+제목 복사]
조회: 26628
글쓴 사람
정성태 (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)
13483정성태12/14/20232316닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232910닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232287개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232661개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232342개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232532닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232270닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/20232335닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/20232182개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/20232393닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
13472정성태12/4/20232198C/C++: 164. Visual C++ - InterlockedCompareExchange128 사용 방법
13471정성태12/4/20232277Copilot - To enable GitHub Copilot, authorize this extension using GitHub's device flow
13470정성태12/2/20232572닷넷: 2178. C# - .NET 8부터 COM Interop에 대한 자동 소스 코드 생성 도입파일 다운로드1
13469정성태12/1/20232297닷넷: 2177. C# - (Interop DLL 없이) CoClass를 이용한 COM 개체 생성 방법파일 다운로드1
13468정성태12/1/20232232닷넷: 2176. C# - .NET Core/5+부터 달라진 RCW(Runtime Callable Wrapper) 대응 방식파일 다운로드1
13467정성태11/30/20232332오류 유형: 882. C# - Unhandled exception. System.Runtime.InteropServices.COMException (0x800080A5)파일 다운로드1
13466정성태11/29/20232500닷넷: 2175. C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션
13465정성태11/28/20232246개발 환경 구성: 689. MSBuild - CopyToOutputDirectory가 "dotnet publish" 시에는 적용되지 않는 문제파일 다운로드1
13464정성태11/28/20232392닷넷: 2174. C# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합파일 다운로드1
13463정성태11/27/20232302오류 유형: 881. Visual Studio - NU1605: Warning As Error: Detected package downgrade
13462정성태11/27/20232344오류 유형: 880. Visual Studio - error CS0246: The type or namespace name '...' could not be found
13461정성태11/26/20232381닷넷: 2173. .NET Core 3/5+ 기반의 COM Server를 registry 등록 없이 사용하는 방법파일 다운로드1
13460정성태11/26/20232333닷넷: 2172. .NET 6+ 기반의 COM Server 내에 Type Library를 내장하는 방법파일 다운로드1
13459정성태11/26/20232315닷넷: 2171. .NET Core 3/5+ 기반의 COM Server를 기존의 regasm처럼 등록하는 방법파일 다운로드1
13458정성태11/26/20232335닷넷: 2170. .NET Core/5+ 기반의 COM Server를 tlb 파일을 생성하는 방법(tlbexp)
13457정성태11/25/20232277VS.NET IDE: 187. Visual Studio - 16.9 버전부터 추가된 "Display inline type hints" 옵션
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...