基于Geneva框架的STS实现--Client端

 

  1. Client端
  2.       只要按约定的方式访问发布的WCF服务,并提供相应的用户信息,就可以获得IP/STS颁发的令牌。

          可以直接添加对STS服务的引用,按普通的WCF访问来处理,但因为利用Geneva框架实现的STS的服务契约是比较底层,消息契约是采用通信单元Message类,就需要客户端进行进一步处理,才能得到安全令牌的实体。 

          下面是服务契约,按WS-Trust标准约定的语法,方法参数中封装了RST,返回值中封装了RSTS。可以在这里对底层消息直接处理。

    IContract
    1 [ServiceContract]
    2 public interface IWSTrustContract
    3 {
    4 [OperationContract(Name = "Cancel", Action = "*", ReplyAction = "*")]
    5 Message Cancel(Message message);
    6 [OperationContract(Name = "Issue", Action = "*", ReplyAction = "*")]
    7 Message Issue(Message message);
    8 [OperationContract(Name = "Renew", Action = "*", ReplyAction = "*")]
    9 Message Renew(Message message);
    10 [OperationContract(Name = "Validate", Action = "*", ReplyAction = "*")]
    11 Message Validate(Message message);
    12 }
    13  

          Geneva框架也提供了访问STS的客户端类WSTrustClient,对基础的WCF进行了封装。

          我这里利用框架现成的客户端类来实现客户端调用,主要是Issue方法。

          先写一个帮助类。

    Helper类
    1 using Microsoft.IdentityModel.Protocols.WSTrust;
    2  namespace STS.Client
    3 {
    4 public class TokenHelper
    5 {
    6 /// <summary>
    7 /// 向STS服务器为一个服务申请令牌并得到令牌信息
    8 /// </summary>
    9 /// <param name="userName">当前登录的用户名</param>
    10 /// <param name="password">密码</param>
    11 /// <returns>安全令牌对象</returns>
    12   public static GenericXmlSecurityToken Issue(string userName, string password)
    13 {
    14 //STS终结点
    15   EndpointAddress ep = new EndpointAddress(STSConfiguration.STSUrl);
    16 //如果服务器证书与服务的域名不一致,需要根据STS证书改写终结点标识
    17 //如果想加一些自定义的头部,则需要指定特定的AddressHeader集合
    18 //Uri uri = new Uri(stsUrl);
    19 //EndpointIdentity identity = EndpointIdentity.CreateX509CertificateIdentity(Configuration.STSCertificate);
    20 //AddressHeader[] headers = new AddressHeader[] { };
    21 //EndpointAddress ep = new EndpointAddress(uri, identity, headers);
    22
    23 //构造函数需要自己指定终结点,WSTrustClient类虽然需要指定配置名,但会忽略配置中的WCF终结点信息
    24 WSTrustClient trustClient = new WSTrustClient("WS2007HttpBinding_IWSTrust13Sync", ep);
    25
    26 trustClient.ClientCredentials.UserName.UserName = userName;
    27 trustClient.ClientCredentials.UserName.Password = password;
    28
    29 //构造RST
    30 RequestSecurityToken rst = new RequestSecurityToken(WSTrust13Constants.RequestTypes.Issue);
    31 rst.AppliesTo = newEndpointAddress(Configuration.ServiceUrl); //令牌的使用方
    32 rst.RequestDisplayToken = true; //如果需要显示令牌,需要置这个标记,当然首先服务端需要实现GetDisplayToken()方法。
    33
    34 //申请安全令牌
    35 SecurityToken token = trustClient.Issue(rst);
    36 return token as GenericXmlSecurityToken;
    37 }
    38 }
    39

         客户端相应的配置文件如下,需要注意,STS服务地址要另外配置:

    app.Config
    1 <system.serviceModel>
    2 <client>
    3 <endpoint name="WS2007HttpBinding_IWSTrust13Sync"
    4 address=""
    5 binding="ws2007HttpBinding" bindingConfiguration="wsHttpUserName"
    6 contract="Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustContract"
    7 behaviorConfiguration="ClientCertificateBehavior">
    8 </endpoint>
    9 </client>
    10 <bindings>
    11 <ws2007HttpBinding>
    12 <binding name="wsHttpUserName">
    13 <security mode="Message">
    14 <!--指定使用Username进行客户端身份验证,并且需要建立安全上下文-->
    15 <message clientCredentialType="UserName" negotiateServiceCredential="false" establishSecurityContext="true" />
    16 </security>
    17 </binding>
    18 </ws2007HttpBinding>
    19 </bindings>
    20 <behaviors>
    21 <endpointBehaviors>
    22 <behavior name="ClientCertificateBehavior">
    23 <clientCredentials>
    24 <serviceCertificate>
    25 <!--指定服务默认证书,以及正式验证模式和吊销模式-->
    26 <defaultCertificate findValue=" CN=sts-server" storeLocation="LocalMachine" storeName="TrustedPeople"/>
    27 <authentication certificateValidationMode="PeerOrChainTrust" revocationMode="NoCheck" />
    28 </serviceCertificate>
    29 </clientCredentials>
    30 </behavior>
    31 </endpointBehaviors>
    32 </behaviors>
    33 </system.serviceModel>
    34  

         这样客户端就可以利用Helper类申请安全令牌。

    Client Main函数
    1 namespace STS.Client
    2 {
    3 class Program
    4 {
    5 static void Main(string[] args)
    6 {
    7 string username = Console.ReadLine();
    8 string password = Console.ReadLine();
    9
    10 SecurityToken token = TokenHelper.Issue(username, password);
    11
    12 Console.WriteLine(token.ToString());
    13 Console.ReadLine();
    14 }
    15 }
    16 }
    17  

     

     

          令牌是基于SAML标准的,类型是GenericXmlSecurityToken。客户端获得令牌后,可以利用令牌访问wsFederationHttpBinding的WCF服务。服务作为RP端,利用服务端证书解密令牌,获取IP/STS所声明的信息(包括访问者身份),达到了联合认证的目的。

          下一篇会写Client端怎样以wsFederationHttpBinding方式访问WCF,WCF又怎样获取令牌中的声明。

          

  3. 令牌文本
  4.       如果客户端需要对令牌做本地持久化或者传输,相比对象的操作就没有文本的操作更有效了。这时我们需要得到安全令牌的XML文本。

          利用Geneva框架,有两种方式可以达到目的:

          一种方式是利用框架客户端类Issue方法的另一个重载Message Issue(Message message),直接操作通信单元,得到SOAP消息文本,进而得到我们想要的内容(在Body节点中)。但我们需要手工实现RST到Message以及Message到RSTR的转换。

         

          我这里使用另一种方式,继承框架客户端的WSTrustClient类,利用Issue()方法的既有功能实现一个新的Issue()方法,将Message的文本跟令牌一起传递出来。

      

    WSTrustClient2类
    1 using Microsoft.IdentityModel;
    2  namespace Wuhong.STS.Client
    3 {
    4 internal class WSTrustClient2 : WSTrustClient//ClientBase<IWSTrustContract>, IWSTrustContract
    5   {
    6 /// <summary>
    7 /// 构造函数
    8 /// </summary>
    9 /// <param name="endpointConfigurationName"></param>
    10 /// <param name="epa"></param>
    11   public WSTrustClient2(string endpointConfigurationName, EndpointAddress epa)
    12 : base(endpointConfigurationName, epa)
    13 {
    14 }
    15
    16 /// <summary>
    17 /// 申请令牌并得到Message主体文本--扩展
    18 /// </summary>
    19 /// <param name="rst">RST</param>
    20 /// <param name="xmlMessage">Message消息body文本</param>
    21 /// <returns>令牌对象</returns>
    22   public SecurityToken Issue(RequestSecurityToken rst, out string xmlMessage)
    23 {
    24 if (rst == null)
    25 {
    26 ArgumentException e = new ArgumentException("申请令牌时参数不正确");
    27 throw e;
    28 }
    29
    30 RequestSecurityTokenResponse rstr = null;
    31 Message message = this.Issue(this.BuildRequestAsMessage(rst, "http://schemas.microsoft.com/idfx/requesttype/issue", this.TrustVersion));
    32 if (message.IsFault)
    33 {
    34 throw FaultException.CreateFault(MessageFault.CreateFault(message, 0x5000), new Type[0]);
    35 }
    36
    37 //这里是与基类方法不同的地方,将Message的文本中body部分传递出去,利用这部分内容也可以重新构造出令牌对象来
    38   xmlMessage = this.GetBody(message.ToString());
    39
    40 rstr = this.WSTrustResponseSerializer.ReadXml(message.GetReaderAtBodyContents(), this.CreateSerializationContext());
    41 return this.GetTokenFromResponse(rst, rstr);
    42 }
    43
    44 /// <summary>
    45 /// 从RSTR消息得到body的文本
    46 /// </summary>
    47 /// <param name="xml">消息文本</param>
    48   private string GetBody(string xml)
    49 {
    50 string re = null;
    51
    52 XmlDocument doc = new XmlDocument();
    53 doc.LoadXml(xml);
    54 XmlNode node = doc.DocumentElement.LastChild;
    55 if (node != null)
    56 {
    57 re = node.InnerXml;
    58 }
    59 return re;
    60 }
    61
    62 /// <summary>
    63 /// 必要的需要实现的私有方法
    64 /// </summary>
    65   private Message BuildRequestAsMessage(RequestSecurityToken request, string requestType, TrustVersion trustVersion)
    66 {
    67 return Message.CreateMessage(base.Endpoint.Binding.MessageVersion ?? MessageVersion.Default, GetAction(requestType, trustVersion), (BodyWriter)new WSTrustRequestBodyWriter(request, this.WSTrustRequestSerializer, this.CreateSerializationContext()));
    68 }
    69
    70 /// <summary>
    71 /// 必要的需要实现的私有方法
    72 /// </summary>
    73   private static string GetAction(string requestType, TrustVersion trustVersion)
    74 {
    75 //方法略过,可以参考反编译的代码……
    76   }
    77
    78 }
    79 }
    80

                 

          Helper类也做相应的修改,就可以得到RSTS的BodyXML文本。

               

          当然也可以得到更多的消息内容,比如RST和RSTR的全部原始内容,因为是基于WCF的通信,所以都是SOAP格式。

          不过现在只关心得到的RSTS的Body文本。因为篇幅的问题,只列出了主要节点。

          <trust:RequestSecurityTokenResponse>就是完整的RSTR

          <trust:KeySize>密钥位长

          <trust:Lifetime>令牌生存期:10小时

          <wsp:AppliesTo>令牌颁发给的RP方

          <trust:RequestedSecurityToken/>这就是请求的令牌,内容是经过XML加密的,不能直接使用,但是提供了加密证书的颁发者和序列号。如果拥有此证书,就可以解密XML文本,获得其中的声明内容。同时XML也利用STS证书经行了签名,防止篡改。

          <i:RequestedDisplayToken/>这就是显示令牌,内容是明文的,如果客户端RST中设置了不需要此内容,这个节点不会存在。在文本中可以看到上一篇在STS服务中写入的声明。

          <trust:RequestedProofToken/>一个证明令牌,提供非对称加密的约定基础值,只有发送方和接受方知道,用来证明所有权。

          <trust:TokenType/>令牌类型:SAML1.0

          <trust:RequestType/>请求类型:Issue

          <trust:KeyType/>密钥类型:对称

    RSTR Body文本主体
    <trust:RequestSecurityTokenResponseCollection>
    <trust:RequestSecurityTokenResponse>
    <trust:KeySize/>
    <trust:Lifetime/>
    <wsp:AppliesTo/>
    <trust:RequestedSecurityToken/>
    <i:RequestedDisplayToken/>
    <trust:RequestedProofToken/>
    <trust:RequestedAttachedReference/>
    <trust:RequestedUnattachedReference/>
    <trust:TokenType/>
    <trust:RequestType/>
    <trust:KeyType/>
    </trust:RequestSecurityTokenResponse>
    </trust:RequestSecurityTokenResponseCollection>

          不要忘记,我们同时还需要再实现一个Issue()方法,从Message文本构造出安全令牌对象。因为框架客户端类提供了GetTokenFromResponse()方法,因此如果我们利用Message文本构造出RSTS,就可以还原令牌对象了。

    WSTrustClient2类
    1 using Microsoft.IdentityModel;
    2 namespace Wuhong.STS.Client
    3 {
    4 internal class WSTrustClient2 : WSTrustClient//ClientBase<IWSTrustContract>, IWSTrustContract
    5 {
    6 /// <summary>
    7 /// 从Message主体文本直接构造Token(不到STS服务取)
    8 /// </summary>
    9 /// <param name="xml">Message主体文本</param>
    10 /// <param name="rst">RST</param>
    11 /// <returns>令牌对象</returns>
    12 public SecurityToken Issue(RequestSecurityToken rst, string xml)
    13 {
    14 if (string.IsNullOrEmpty(xml))
    15 {
    16 ArgumentException e = new ArgumentException("无效的令牌文本");
    17 throw e;
    18 }
    19 if (rst == null)
    20 {
    21 ArgumentException e = new ArgumentException("申请令牌时参数不正确");
    22 throw e;
    23 }
    24
    25 RequestSecurityTokenResponse rstr = null;
    26 using (StringReader reader = new StringReader(xml))
    27 {
    28 using (XmlReader reader1 = XmlReader.Create(reader))
    29 {
    30 rstr = this.WSTrustResponseSerializer.ReadXml(reader1, this.CreateSerializationContext());
    31 }
    32 }
    33 return this.GetTokenFromResponse(rst, rstr);
    34 }
    35
    36 }
    37 }
    38

      

          相应的帮助类也新增一个方法:

    Helper类
    1 using Microsoft.IdentityModel;
    2  namespace Wuhong.STS.Client
    3 {
    4 public class TokenHelper
    5 {
    6 /// <summary>
    7 /// 从令牌文本生成令牌对象
    8 /// </summary>
    9 /// <param name="xmlMessage">令牌的文本</param>
    10 /// <returns>令牌对象</returns>
    11   public static GenericXmlSecurityToken Issue(string xmlMessage)
    12 {
    13 EndpointAddress ep = new EndpointAddress(STSConfiguration.STSUrl);
    14
    15 WSTrustClient2 trustClient = new WSTrustClient2("WS2007HttpBinding_IWSTrust13Sync", ep);
    16
    17 //构造令牌申请对象
    18   RequestSecurityToken rst = new RequestSecurityToken(WSTrust13Constants.RequestTypes.Issue);
    19 rst.AppliesTo = new EndpointAddress(STSConfiguration.ServiceUrl);
    20 rst.RequestDisplayToken = true;
    21
    22 //生成令牌
    23 SecurityToken token = trustClient.Issue(rst, xmlMessage);
    24
    25 return token as GenericXmlSecurityToken;
    26 }
    27 }
    28 }
    29

      

  5. 令牌解密
  6.       如果client端本身就是RP方,并且拥有解密证书,那么可以以手工的方式解密安全令牌,直接使用里面的声明。如果RP方是WCF并且使用了wsFederationHttpBinding绑定,WCF会自动解密令牌。这个内容在后面会写。

          解密的对象是是RSTR文本中的<trust:RequestedSecurityToken>节点,我们直接针对上一节得到的XML文本进行操作。

          在Helper类中添加方法:

    Helper类
    1 namespace STS.Client
    2 {
    3 public class TokenHelper
    4 {
    5 /// <summary>
    6 /// 消息解密,以得到明文的内容
    7 /// </summary>
    8 /// <param name="xml">已加密令牌主体XML</param>
    9 /// <returns>解密后的安全令牌XML</returns>
    10 public static string Decrypt(string xml)
    11 {
    12 string cername = "", cersn = "";
    13
    14 XmlDocument doc = new XmlDocument();
    15 doc.InnerXml = xml;
    16
    17 XmlNamespaceManager xm = new XmlNamespaceManager(doc.NameTable);
    18 xm.AddNamespace("trust", "http://docs.oasis-open.org/ws-sx/ws-trust/200512");
    19 xm.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
    20 xm.AddNamespace("xenc", "http://www.w3.org/2001/04/xmlenc#");
    21 xm.AddNamespace("e", "http://www.w3.org/2001/04/xmlenc#");
    22 xm.AddNamespace("o", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
    23 xm.AddNamespace("ki", "http://www.w3.org/2000/09/xmldsig#");
    24
    25 XmlNode keyInfo = doc.SelectSingleNode("//trust:RequestSecurityTokenResponse//trust:RequestedSecurityToken//xenc:EncryptedData//ki:KeyInfo//e:EncryptedKey//ki:KeyInfo", xm);
    26
    27 //获取证书颁发者和序列号
    28 XmlNode cn = keyInfo.SelectSingleNode("//o:SecurityTokenReference//ki:X509Data//ki:X509IssuerSerial//ki:X509IssuerName", xm);
    29 XmlNode cs = keyInfo.SelectSingleNode("//o:SecurityTokenReference//ki:X509Data//ki:X509IssuerSerial//ki:X509SerialNumber", xm);
    30 cername = cn.InnerXml;
    31 cersn = cs.InnerXml;
    32
    33 //设置密钥名称
    34 string keyName = "rsaKey";
    35 keyInfo.InnerXml = string.Format("<KeyName>{0}</KeyName>", keyName);
    36
    37 //解密XML文本
    38 DecryptDocument(doc, cername, cersn, keyName);
    39
    40 return doc.InnerXml;
    41 }
    42
    43 /// <summary>
    44 /// 对XmlDocument执行解密
    45 /// </summary>
    46 /// <param name="doc">XmlDocument对象</param>
    47 /// <param name="cername">证书名称</param>
    48 /// <param name="cersn">证书的序列号(已被Asn1IntegerConverter转换过的)</param>
    49 /// <param name="keyname">加密的标签名</param>
    50 private static void DecryptDocument(XmlDocument doc, string cername, string cersn, string keyname)
    51 {
    52 X509Certificate2 x = CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, cername);
    53 string thisSN = Asn1IntegerConverter.Asn1IntegerToDecimalString(x.GetSerialNumber());
    54 if (thisSN != cersn)
    55 throw new Exception("无法分析文本");
    56
    57 EncryptedXml exml = new EncryptedXml(doc);
    58
    59 exml.AddKeyNameMapping(keyname, x.PrivateKey);
    60
    61 exml.DecryptDocument();
    62 }
    63
    64 }
    65 }
    66

      

          客户端准备好证书,就可以看到安全令牌解密后的内容了。 下面也只列出主要节点:

          <saml:Conditions>声明的一些约束信息,包括令牌起止时间,RP方等。

          <saml:AttributeStatement>声明内容

          <saml:Subject>主体,令牌加密证书的信息

          <saml:Attribute>IP-STS所作的一系列声明,包括属性值和命名空间。这里可以看到在上一篇STS服务写入的两条声明

          <ds:Signature>令牌的签名信息

    SecurityToken解密后文本主体
    1 <trust:RequestedSecurityToken>
    2 <saml:Assertion>
    3 <saml:Conditions>
    4 <saml:AudienceRestrictionCondition>
    5 <saml:Audience></saml:Audience>
    6 </saml:AudienceRestrictionCondition>
    7 </saml:Conditions>
    8 <saml:AttributeStatement>
    9 <saml:Subject>
    10 </saml:Subject>
    11 <saml:Attribute AttributeName="name" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
    12 <saml:AttributeValue>wuhong</saml:AttributeValue>
    13 </saml:Attribute>
    14 <saml:Attribute AttributeName="isadmin" AttributeNamespace="http://sts-server/claims">
    15 <saml:AttributeValue>true</saml:AttributeValue>
    16 </saml:Attribute>
    17 <!—其他声明信息 -->
    18 </saml:AttributeStatement>
    19 <ds:Signature>
    20 <ds:SignedInfo/>
    21 <ds:SignatureValue/>
    22 <KeyInfo/>
    23 </ds:Signature>
    24 </saml:Assertion>
    25  </trust:RequestedSecurityToken>
    26  
posted @ 2010-11-22 16:18  reni  阅读(960)  评论(1编辑  收藏  举报