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