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