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 입니다.)