问题
I have been all over the net for this. I've just been having a devil of a time doing it, and the vendor whose web service I'm trying to consume refuses to officially support WCF as a method of consumption.
I'm no web services expert, so I'll do my best to document and explain with this initial post, but by all means, request more information if you need it, and hopefully I'll be able to supply whatever is necessary.
The service
At my company, we use a vendor application that exposes a service. The application is written in java, and it looks like the wsdl was created with Apache Axis 1.2.
The code
My legacy code uses WSE 3.0. In particular, it uses the proxy classes that have "WSE" auto-tacked at the end. This allows me to use a much simpler authentication scheme (the only way I could get it to work). I don't need to use certificates. I use a derivative of SecurityPolicyAssertion
, and wrap it in a Policy
object that gets passed to the SetPolicy
method of the client class. Here's all I need to do to create a working instance of the client:
MyWebServiceWse api = new MyWebServiceWse();
api.Url = myUrl;
api.SetPolicy(new Policy(new MyDerivedSecurityAssertion(user, pass)));
My default, out-of-the-box code for WCF (generated with a service reference) does not accept credentials, so I know there's a problem right off the bat. I've read various things online about using different security
or binding settings in my app.config
, but nothing has ever completely worked. My most common error after copious tinkering is WSDoAllReceiver: Request does not contain required Security header
.
Here's the app.config. Perhaps we could start by telling me what ought to change here to facilitate passing the credentials--again, I've seen varying opinions online.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="MySoapBinding" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None" proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://xyz:12345/services/MyService"
binding="basicHttpBinding" bindingConfiguration="MySoapBinding"
contract="MyNS.MyService" name="MyService" />
</client>
</system.serviceModel>
</configuration>
I have changed some of the attributes to obscure the specific service we are using (company policy and all that).
And here is the sample C# code so far (testing in a console app):
MyClient client = new MyClient();
client.listMethod();
UPDATE
Read this SO post: wcf security . . ..
I have updated my app.config accordingly, and am now passing username and pwd in code. I am still receiving the same error:
WSDoAllReceiver: Request does not contain required Security header
20120517 UPDATE
A successful request (from WSE3):
<soap:Header>
<wsa:Action>
</wsa:Action>
<wsa:MessageID>urn:uuid:cb739422-c077-4eec-8cb2-686837b76878</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:To>http://removed-for-security</wsa:To>
<wsse:Security soap:mustUnderstand="1">
<wsu:Timestamp wsu:Id="Timestamp-e13feaf9-33d9-47bf-ab5b-60b4611eb81a">
<wsu:Created>2012-05-17T11:25:41Z</wsu:Created>
<wsu:Expires>2012-05-17T11:30:41Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SecurityToken-00c26e1a-3b3b-400f-a99a-3aa54cf8c8ff">
<wsse:Username>change-to-protect-the-innocent</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">nice-try</wsse:Password>
<wsse:Nonce>KJMvUuWF2eO2uIJCuxJC4A==</wsse:Nonce>
<wsu:Created>2012-05-17T11:25:41Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
<soap:Body>
<listChannels xmlns="http://removed-for-security">
<rowfrom>0</rowfrom>
<rowto>10</rowto>
</listChannels>
</soap:Body>
</soap:Envelope>
Working on getting the WCF trace--will add shortly.
20120517 UPDATE 2
And here's the envelope from WCF:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none"></Action>
</s:Header>
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<listChannels xmlns="http://removed-for-security">
<rowfrom>1</rowfrom>
<rowto>2147483647</rowto>
</listChannels>
</s:Body>
</s:Envelope>
20120518 UPDATE I have tried implementing the solution in the post that Mike Miller links to in the comments. Now I receive the following error (no message ends up getting sent because something's barfing on the scheme):
The provided URI scheme 'http' is invalid; expected 'https'.
And in case anyone wants to ask, yes, I need to send over http, and yes, I'm aware that credentials are sent as unencrypted strings :-)
回答1:
What you need is to send a username token over http transport which is not supported in wcf ootb. in addition your token uses nonce/created which is also not ootb. you have 2 options:
this oss project adds nonce/created to the username token. this oss project adds the ability to send username over http. you would need to combine both projects together.
ws-security is usually considered complex, but you use it in its simplest form (username). the easiest would be to dismiss any wcf security setting all together and create the whole security header by yourself in a message inspector! As you can see most headers are just static xml nodes, and most values are pretty clear (you know the username). the only tricky two are the nonce and the timestamps which you could look how to do in this oss project (one line each). There is a variant of this option which may be easier - use CUB after all and implement a custom encoder which pushes the timestmpa/nonce. I would go for the latter but I'm biased since I developed CUB...
There's also the ws-addressing headers which you can configure on your custom encoding "messageVersion" property. I can't tell the exact value since you omitted the envelope header with the wsa prefix definition.
If you want help privately (since you seem to have security restrictions) by all means send me an email from my blog.
EDIT: I've implemented it for you. follow these steps:
download cub and make yourself familiar with it (not the internals, just how to use it according to the blog post)
add reference to System.Runtime.Serialization.dll to the project ClearUsernameBinding
add a new file to that project: UsernameExEncoder.cs. Paste this content:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel.Channels; using System.IO; using System.Xml; using System.Security.Cryptography; namespace Webservices20.BindingExtensions { class UsernameExEncoderBindingElement : MessageEncodingBindingElement { MessageEncodingBindingElement inner; public UsernameExEncoderBindingElement(MessageEncodingBindingElement inner) { this.inner = inner; } public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context) { context.BindingParameters.Add(this); var res = base.BuildChannelFactory<TChannel>(context); return res; } public override bool CanBuildChannelFactory<TChannel>(BindingContext context) { var res = base.CanBuildChannelFactory<TChannel>(context); return res; } public override MessageEncoderFactory CreateMessageEncoderFactory() { return new UsernameExEncoderFactory(this.inner.CreateMessageEncoderFactory()); } public override MessageVersion MessageVersion { get { return this.inner.MessageVersion; } set { this.inner.MessageVersion = value; } } public override BindingElement Clone() { var c = (MessageEncodingBindingElement)this.inner.Clone(); var res = new UsernameExEncoderBindingElement(c); return res; } public override T GetProperty<T>(BindingContext context) { var res = this.inner.GetProperty<T>(context); return res; } } class UsernameExEncoderFactory : MessageEncoderFactory { MessageEncoderFactory inner; public UsernameExEncoderFactory(MessageEncoderFactory inner) { this.inner = inner; } public override MessageEncoder Encoder { get { return new UsernameExEncoder(inner.Encoder); } } public override MessageVersion MessageVersion { get { return this.inner.MessageVersion; } } } class UsernameExEncoder : MessageEncoder { MessageEncoder inner; public override T GetProperty<T>() { return inner.GetProperty<T>(); } public UsernameExEncoder(MessageEncoder inner) { this.inner = inner; } public override string ContentType { get { return this.inner.ContentType; } } public override string MediaType { get { return this.inner.MediaType; } } public override MessageVersion MessageVersion { get { return this.inner.MessageVersion; } } public override bool IsContentTypeSupported(string contentType) { return this.inner.IsContentTypeSupported(contentType); } public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType) { return this.inner.ReadMessage(buffer, bufferManager, contentType); } public override Message ReadMessage(System.IO.Stream stream, int maxSizeOfHeaders, string contentType) { return this.inner.ReadMessage(stream, maxSizeOfHeaders, contentType); } public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) { //load the message to dom var mem = new MemoryStream(); var x = XmlWriter.Create(mem); message.WriteMessage(x); x.Flush(); mem.Flush(); mem.Position = 0; XmlDocument doc = new XmlDocument(); doc.Load(mem); //add the missing elements var token = doc.SelectSingleNode("//*[local-name(.)='UsernameToken']"); var created = doc.CreateElement("Created", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); var nonce = doc.CreateElement("Nonce", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); token.AppendChild(created); token.AppendChild(nonce); //set nonce value byte[] nonce_bytes = new byte[16]; RandomNumberGenerator rndGenerator = new RNGCryptoServiceProvider(); rndGenerator.GetBytes(nonce_bytes); nonce.InnerText = Convert.ToBase64String(nonce_bytes); //set create value created.InnerText = XmlConvert.ToString(DateTime.Now.ToUniversalTime(), "yyyy-MM-ddTHH:mm:ssZ"); //create a new message var r = XmlReader.Create(new StringReader(doc.OuterXml)); var newMsg = Message.CreateMessage(message.Version, message.Headers.Action, r); return this.inner.WriteMessage(newMsg, maxMessageSize, bufferManager, messageOffset); } public override void WriteMessage(Message message, System.IO.Stream stream) { this.inner.WriteMessage(message, stream); } } }
In the file ClearUsernameBinding.cs replace this:
res.Add(new TextMessageEncodingBindingElement() { MessageVersion = this.messageVersion});
with this:
var textEncoder = new TextMessageEncodingBindingElement() { MessageVersion = this.messageVersion }; res.Add(new UsernameExEncoderBindingElement(textEncoder));
In the project TestClient in app.config there is a messageVersion property on the binding element. You have not published the root of your envelope so I cannot know for sure, but probably you need to set it to Soap11WSAddressingAugust2004 or Soap11WSAddressing10 (or one of these with Soap12 instead).
Good luck!
来源:https://stackoverflow.com/questions/10589561/attempting-to-migrate-from-wse-3-0-to-wcf-for-client-code