基于Geneva框架的STS实现--Client端
- Client端
- 令牌文本
- 令牌解密
只要按约定的方式访问发布的WCF服务,并提供相应的用户信息,就可以获得IP/STS颁发的令牌。
可以直接添加对STS服务的引用,按普通的WCF访问来处理,但因为利用Geneva框架实现的STS的服务契约是比较底层,消息契约是采用通信单元Message类,就需要客户端进行进一步处理,才能得到安全令牌的实体。
下面是服务契约,按WS-Trust标准约定的语法,方法参数中封装了RST,返回值中封装了RSTS。可以在这里对底层消息直接处理。
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方法。
先写一个帮助类。
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服务地址要另外配置:
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类申请安全令牌。
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又怎样获取令牌中的声明。
如果客户端需要对令牌做本地持久化或者传输,相比对象的操作就没有文本的操作更有效了。这时我们需要得到安全令牌的XML文本。
利用Geneva框架,有两种方式可以达到目的:
一种方式是利用框架客户端类Issue方法的另一个重载Message Issue(Message message),直接操作通信单元,得到SOAP消息文本,进而得到我们想要的内容(在Body节点中)。但我们需要手工实现RST到Message以及Message到RSTR的转换。
我这里使用另一种方式,继承框架客户端的WSTrustClient类,利用Issue()方法的既有功能实现一个新的Issue()方法,将Message的文本跟令牌一起传递出来。
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/>密钥类型:对称
<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,就可以还原令牌对象了。
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
相应的帮助类也新增一个方法:
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
如果client端本身就是RP方,并且拥有解密证书,那么可以以手工的方式解密安全令牌,直接使用里面的声明。如果RP方是WCF并且使用了wsFederationHttpBinding绑定,WCF会自动解密令牌。这个内容在后面会写。
解密的对象是是RSTR文本中的<trust:RequestedSecurityToken>节点,我们直接针对上一节得到的XML文本进行操作。
在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>令牌的签名信息
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