以前项目解决方案中,用http协议的asmx Web service作服务器数据访问入口,在SoapHeader中写入用户名和加盐密码进行身份认证。
http asmx服务是明文传输,传输过程中数据很容易被截取、篡改。在内网使用、用户量小、安全问题不严重的情况下运行几年,没有出过大的问题。
但随着项目的发展,需要对服务器进行改造,升级成更高级的安全方式。
最先想到的是将http协议改用https,解决数据明文传输泄密以及有可能被篡改,造成服务器数据严重混乱问题。
但是,https传输存在两个问题:1是客户机要安装证书,增加用户操作复杂度;2是运行环境中客户机与服务器之间可能存在安全接入边界系统,传输层加密存在诸多问题和不确定性。
因此,决定采用WCF Message加密方案,不使用Transport安全模式,保持http传输协议。这样客户机与服务器通信数据可以顺利通过安全接入边界系统,一套方案可以适应各种网络结构、适应不同的网络环境,安装、布署、使用简单,对技术支持、维护人员培训成本低。虽然可能Message安全方案效率低些,但在网络速度飞速提升的背景下,对用户体验不会造成大的影响。
另外,采用Windows Service寄宿方案,不涉及IIS的安装、配置,降低技术维护复杂性,对安装、维护人员要求也低些,相对来讲,可控度更高,服务器安全性、稳定性也有提升。
实现WCF Message安全方案:
一,服务证书准备
1,创建证书:Visual Studio Tools命令提示符下执行(假设证书名为WCFServerCA,实际可以替换为自己想要名)
makecert -r -pe -n "CN=WCFServerCA" -sr LocalMachine -ss My -sky exchange
2,导出证书:运行MMC控制台,从本地计算机节点证书内导出WCFServerCA证书,保存为带私钥的证书文件,扩展名为.pfx。导出时要输入密码,该密码要在导入时使用。
3,导入证书:在运行服务的计算机上导入该.pfx证书,导入到本地计算机节点受信任的根证书颁发机构和受信任人子节点下。此证书可以保存起来,供其他服务器安装时使用。
二,建契约项目 Contract。服务契约单独出来,由服务、宿主、客户端引用,在编程上自由度更高。

namespace Contract { [ServiceContract] public interface IService { [OperationContract] string Hello(string name); } }
三,建WCF服务库项目 WcfLib。包含服务类和UserName验证类。
OperationContext.Current是WCF中的重要对象,代表服务操作上下文。通过此对象,可以获取与服务操作有关的各种数据,如用OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name获取到客户端验证用户名。
服务实现:

using System.ServiceModel; using System.ServiceModel.Channels; namespace WcfLib { public class Service : IService { public string Hello(string name) { //提供方法执行的上下文环境 OperationContext context = OperationContext.Current; //UserName验证的用户名 string username = context.ServiceSecurityContext.PrimaryIdentity.Name; //获取传进的消息属性 MessageProperties properties = context.IncomingMessageProperties; //获取消息发送的远程终结点IP和端口 RemoteEndpointMessageProperty endpoint = properties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty; Console.WriteLine(string.Format("Hello {0}, You are from {1}:{2}", name, endpoint.Address, endpoint.Port)); return string.Format("Hello {0},You are from {1}:{2}", name, endpoint.Address, endpoint.Port); } } }
UserName验证实现:

namespace WcfLib { public class CustomUserNamePasswordValidator : UserNamePasswordValidator { private const string FAULT_EXCEPTION_MESSAGE = "用户登录失败!"; public override void Validate(string userName, string password) { bool validateCondition = false; validateCondition = userName == "z" && password == "123"; if (!validateCondition) { throw new FaultException(FAULT_EXCEPTION_MESSAGE); } } } }
服务配置文件:

<system.serviceModel> <services> <!--name需为服务类的完全限定名--> <service name="WcfLib.Service" behaviorConfiguration="MessageAndUserNameBehavior"> <host> <baseAddresses> <!--服务基地址,应以/结尾,以便与endpoint的address组合--> <add baseAddress = "http://localhost:8733/" /> </baseAddresses> </host> <!-- Service Endpoints --> <!-- 除非完全限定,否则地址相对于上面提供的基址--> <endpoint address="" binding="wsHttpBinding" bindingConfiguration="MessageAndUserName" contract="Contract.IService"> <!--服务器和客户端dns标识value值应一致,如删除,客户端dns值应与证书名称相符(更新服务引用时可自动获取) --> <identity> <dns value="WCFServerCA"/> </identity> </endpoint> <!-- Metadata Endpoints --> <!-- 元数据交换终结点供相应的服务用于向客户端做自我介绍。 --> <!-- 此终结点不使用安全绑定,应在部署前确保其安全或将其删除--> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services> <bindings> <wsHttpBinding> <binding name="MessageAndUserName"> <!--使用消息安全自定义用户名和密码验证--> <security mode="Message"> <message clientCredentialType="UserName"/> </security> </binding> </wsHttpBinding> </bindings> <behaviors> <serviceBehaviors> <behavior name="MessageAndUserNameBehavior"> <!-- 为避免泄漏元数据信息, 请在部署前将以下值设置为 false --> <serviceMetadata httpGetEnabled="True" httpsGetEnabled="True"/> <!-- 要接收故障异常详细信息以进行调试, 请将以下值设置为 true。在部署前设置为 false 以避免泄漏异常信息 --> <serviceDebug includeExceptionDetailInFaults="False" /> <!--指定服务证书--> <serviceCredentials> <!--根据证书名称查找服务证书 到TrustedPeople受信任人里面去查找--> <serviceCertificate storeName="TrustedPeople" x509FindType="FindBySubjectName" findValue="WCFServerCA" storeLocation="LocalMachine" /> <!--对服务证书的认证模式 本配置为服务端,当windows service寄宿时,到可信任人区去查找认证--> <clientCertificate> <authentication certificateValidationMode="PeerTrust"/> </clientCertificate> <!--自定义用户密码身份验证程序集--> <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="WcfLib.CustomUserNamePasswordValidator,WcfLib"/> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
五,实现Windows Service寄宿。
1,新建控制台项目WinServiceHosting,添加 System.ServiceModel.dll 的引用,添加 WCF 服务类库(WcfLib)的项目引用(此例服务类库名WcfServiceLib),添加服务契约项目Contract引用。
2,项目中添加Window服务类,起名WCFServiceMgr.cs,服务寄宿代码:

using System; using System.ServiceModel; using System.ServiceProcess; namespace WinServiceHosting { partial class WCFServiceMgr : ServiceBase { public static string Name = "WCF服务Windows Service寄宿"; static object syncRoot = new object();//同步锁 ServiceHost srvHost = null; //寄宿服务对象 public WCFServiceMgr() { InitializeComponent(); this.ServiceName = Name; } protected override void OnStart(string[] args) { // 启动服务。 try { srvHost = new ServiceHost(typeof(WcfSumServiceLib.Sum)); if (srvHost.State != CommunicationState.Opened) { srvHost.Open(); //此处写启动日志 } } catch (Exception) { //此处写错误日志 } } protected override void OnStop() { // 停止服务 if (srvHost!=null) { srvHost.Close(); srvHost = null; //写服务停止日志 } } } }
3,在WCFServiceMgr设计界面上右键,添加安装程序,项目里生成ProjectInstaller.cs文件。
4,选中serviceInstaller1,设定服务标识名称DisplayName:WCF服务Windows Service寄宿(此名称在控制面板服务中显示);再设定系统服务标识名称ServiceName:WCFServiceMgr(此名称用于系统标识);再设定服务启动方式StartType为Automatic,即自动启动。选中serviceProcessInstaller1,指定用来运行此服务的帐户类型Account为LocalSystem。
5,通过控制台程序实现参数化安装和卸载服务 , -i表示安装,-u表示卸载。服务配置与服务类库配置相同

using System; using System.Configuration.Install; using System.ServiceProcess; namespace WinServiceHosting { class Program { static void Main(string[] args) { ServiceController service = new ServiceController(WCFServiceMgr.Name); if (args.Length==0) { //运行服务 ServiceBase[] serviceBasesToRun = new ServiceBase[] { new WCFServiceMgr() }; ServiceBase.Run(serviceBasesToRun); } else if(args[0].ToLower()=="/i" || args[0].ToLower()=="-i") { //安装服务 if (!IsServiceExisted("WCFServiceMgr")) { try { string[] cmdline = { }; string serviceFileName = System.Reflection.Assembly.GetExecutingAssembly().Location; TransactedInstaller transactedInstaller = new TransactedInstaller(); AssemblyInstaller assemblyInstaller = new AssemblyInstaller(serviceFileName, cmdline); transactedInstaller.Installers.Add(assemblyInstaller); transactedInstaller.Install(new System.Collections.Hashtable()); TimeSpan timeout = TimeSpan.FromMilliseconds(1000 * 10); service.Start(); service.WaitForStatus(ServiceControllerStatus.Running, timeout); } catch (Exception) { } } } else if (args[0].ToLower() == "/u" || args[0].ToLower() == "-u") { //删除服务 try { if (IsServiceExisted("WCFServiceMgr")) { string[] cmdline = { }; string serviceFileName = System.Reflection.Assembly.GetExecutingAssembly().Location; TransactedInstaller transactedInstaller = new TransactedInstaller(); AssemblyInstaller assemblyInstaller = new AssemblyInstaller(serviceFileName, cmdline); transactedInstaller.Installers.Add(assemblyInstaller); transactedInstaller.Uninstall(null); } } catch (Exception ) { } } } #region 检查服务存在的存在性 /// <summary> /// 检查服务存在的存在性 /// </summary> /// <param name=" NameService ">服务名</param> /// <returns>存在返回 true,否则返回 false;</returns> public static bool IsServiceExisted(string NameService) { ServiceController[] services = ServiceController.GetServices(); foreach (ServiceController s in services) { if (s.ServiceName.ToLower() == NameService.ToLower()) { return true; } } return false; } #endregion } }
6,制作两个bat脚本,放在程序运行目录下,实现自动安装和卸载
install.bat
WinServiceHosting.exe -i
pause
uninstall.bat
WinServiceHosting.exe -u
pause
六,客户端
1,在服务器上执行install.bat安装并启动服务,按照服务地址,在项目中添加服务引用,自动生成服务代理类和配置文件
2,由于设定客户端免装证书,不进行服务器认证,配置文件如下:

<system.serviceModel> <bindings> <wsHttpBinding> <binding name="WSHttpBinding_IService"> <security> <message clientCredentialType="UserName" /> </security> </binding> </wsHttpBinding> </bindings> <client> <endpoint address="http://192.168.126.129:8733/" behaviorConfiguration="ClientWithoutCABehavior" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService" contract="ServiceReference1.IService" name="WSHttpBinding_IService"> <identity> <!-dns要与服务器配置相同,如服务器无此配置,要与证书名相同--> <dns value="WCfServerCA" /> </identity> </endpoint> </client> <behaviors> <endpointBehaviors> <behavior name ="ClientWithoutCABehavior"> <!--客户端免装证书,所以不进行服务证书认证--> <clientCredentials> <serviceCertificate> <authentication certificateValidationMode="None"/> </serviceCertificate> </clientCredentials> </behavior> </endpointBehaviors> </behaviors> </system.serviceModel>
3,客户端测试调用服务代码:

using ConsoleClient.ServiceReference1; using System; namespace ConsoleClient { class Program { static void Main(string[] args) { using (ServiceClient proxy=new ServiceClient("WSHttpBinding_IService")) { //调用服务前写入用户名密码供服务器验证 string strUserName = "z"; proxy.ClientCredentials.UserName.UserName = strUserName; proxy.ClientCredentials.UserName.Password = "123"; //调用服务 string strMessage = proxy.Hello(strUserName); Console.WriteLine(strMessage); } Console.WriteLine("press any key to continue。。。"); Console.ReadKey(); } } }
Over !
参考:
消息安全模式之UserName客户端身份验证(参考:老徐的博客)