代码改变世界

HTTP Communication and Security with Silverlight

2009-04-11 08:48  刘少侠  阅读(552)  评论(1)    收藏  举报
SilverLight作为一个精简的WPF和.NET子集,要开发一个相对复杂的应用,还是受限颇多,不知道银光合时才能真正照亮WEB,期待SL 4。
 

Silverlight enables HTTP/HTTPS communication with Web services hosted both within and outside the domain that is hosting your Silverlight-based application. This topic discusses several HTTP communication scenarios and how you can enable these scenarios.

 

 

This topic contains the following sections.

There are some basic capabilities for all Silverlight HTTP/HTTPS communication.

  • Same-domain calls are always allowed.

  • When the Web server hosting the Web services is appropriately configured, cross-domain and cross-scheme calls are supported.

  • All communication is asynchronous.

  • Only GET and POST verbs are supported.

  • Most standard and all custom request headers are supported. (Headers must be allowed in the cross-domain policy file before they can be set on cross-domain requests.)

  • Only 200 OK and 404 Not Found status codes are available.

Silverlight supports several scenarios that use HTTP/HTTPS. Although there are multiple ways and technologies that can be used to make HTTP calls, the following table describes possible approaches for some communication scenarios. These approaches are discussed in more detail later in this document.

Scenario

Recommended Approach

Download and upload resources in the same domain.

Use the WebClient class. For more information, see Downloading Content on Demand.

Call HTTP-based Web services hosted in the same domain.

Use the WebClient class or the HttpWebRequest/HttpWebResponse classes. For more information, see How to: Make Requests to HTTP-Based Services.

Call SOAP, WCF, or ASP.NET AJAX Web services hosted in the same domain.

Call the generated proxy for the Web service. For more information, see Accessing Services using Generated Proxies.

If you do not want to use a proxy, use the HttpWebRequest/HttpWebResponse classes.

Process XML, JSON, or RSS data from a Web service.

Use the WebClient class or the HttpWebRequest/HttpWebResponse classes. For more information, see Accessing HTTP Services Directly or How to: Load an XML File from an Arbitrary URI Location with LINQ to XML.

Call a Web service that is on a different domain.

Ensure a cross-domain policy file is at the root of the domain. Use a proxy, the WebClient class, or the HttpWebRequest/HttpWebResponse classes. For more information, see How to: Make a Service Available Across Domain Boundaries.

Set request headers on cross-domain post requests.

Ensure the header is allowed per the cross-domain policy file.

For request headers on data uploads, use the WebClient class. Set its Headers property to the desired header collection.

For other scenarios use the HttpWebRequest class. Set its Headers property to the desired header collection. For a list of allowed headers, see HttpWebRequest..::.Headers.

By default, Silverlight supports calls to Web services on the same domain or site of origin. Same domain means that calls must use the same sub domain, protocol, and port. This is for security reasons and prevents cross-domain forgery.

The following illustration shows examples of calls that are allowed and not allowed in a Silverlight-based application that uses the default settings.

 

 

Same Domain and Cross-Domain Example

Silverlight default networking restrictions

Silverlight supports calls to Web services on a different domain, called cross-domain communication. You can enable Web services to be called by a Silverlight-based application in another domain by deploying a Web service that uses the correct cross-domain policy file at the root of the domain. Silverlight supports two types of cross-domain policy files.

  • Silverlight cross-domain policy file (clientaccesspolicy.xml)

  • A subset of the Adobe Flash cross-domain policy file (crossdomain.xml)

The following illustration shows an example of cross-domain communication.

Cross-Domain Communication by using a Cross-Domain Policy File

Silverlight cross-domain policy

In general, when a Silverlight-based application detects that its request is a cross-domain request, it will first look for the Silverlight cross-domain policy file (clientaccesspolicy.xml) at the application root of the Web service. If this request causes a 404 Not Found or other error, the application will then look for the Adobe Flash cross-domain policy file (crossdomain.xml) at the application root. Redirects for the cross-domain policy file are not allowed. In addition, the cross-domain policy file is requested only once per application session.

The following table lists request URIs and where the Silverlight-based application will look for the cross-domain policy file.

Request URI

Cross-Domain Policy File Location

http://contoso.com/services/data

http://contoso.com/clientaccesspolicy.xml

http://sales.contoso.com/services/data

http://sales.contoso.com/clientaccesspolicy.xml

http://contoso.com:8080/services/data

http://contoso.com:8080/clientaccesspolicy.xml

https://contoso.com/services/data

https://contoso.com/clientaccesspolicy.com

As mentioned previously, redirects on cross-domain policy files are not allowed. However, a Silverlight-based application will follow a redirect for a target resource. The resource can be retrieved only if access is granted by the following:

  • The cross-domain policy file at the domain indicated by the original URI before redirection.

  • The cross-domain policy file at the domain indicated by the final URI after all redirections.

Security Considerations for Web Services Exposed to Silverlight Cross-Domain Callers

There are important security considerations before you allow Silverlight clients to access Web services in a cross-domain situation. Whenever you put a cross-domain policy file in place you should configure your Web server hosting the Web services to disable browser caching. This enables you to easily update the file or restrict access to your Web services if necessary. Once the cross-domain policy file is checked, it remains in effect for the browser session so the impact of non-caching to the end-user is minimal.

In addition, all Silverlight requests are sent with cookies and authentication. This means that if you have Web services that allow users to access private information, you should host these in a different domain than the Web services exposed to third-party callers. For example, consider that you have a Web store hosted at http://www.tailspintoys.com. Your site allows customers to store billing information that includes credit card numbers. You should not expose a Web service that returns product inventory to third-party Silverlight clients at the same domain. Because cookies and authentication are sent with each message, if you host these Web services on the same domain, you have effectively given the third-party callers access to your customer's private billing information. In this example, your publicly exposed Web services could safely be hosted at http://services.tailspintoys.com, because this is a different domain. You must carefully consider who you have exposed Web services to, and what other Web services are located at that domain. Also, you should always keep your cross-domain policy file as restrictive as possible. For more information about exposing secure Web services, see Security Considerations for Service Access and Making a Service Available Across Domain Boundaries.

Cross-Domain Policy File Example

The Silverlight cross-domain policy file is an XML file that has a simple format. The following example shows a Silverlight cross-domain policy file that allows requests to a Web service located at the services path of the domain. The cross-domain policy file also enables SOAPAction Content-Type headers.

<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy >
<allow-from http-request-headers="SOAPAction">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/services/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>

WildCard Character in the Silverlight Cross-Domain Policy File

The Silverlight cross-domain policy file (clientaccesspolicy.xml) can contain a wildcard character (*). The wildcard character can be used to give all domains access to your services, but in general you should limit cross-domain access to a minimal set of domains that you know will require access. The following table lists the possible values for the domain node and the effect each value has during cross-scheme access.

Web Service Scheme

<domain uri = *>

<domain uri= http://*>

<domain uri = http://someDomain.com >

HTTPS

Enables all HTTPS callers

Enables all HTTP callers

Callers from http://someDomain.com only

HTTP

Enables all HTTP and HTTPS callers

Enables all HTTP callers

Callers from http://someDomain.com only

Header Support for Cross-Domain Requests

In general, request headers may not be set on cross-domain GET requests and request headers are permitted on cross-domain POST requests only when explicitly permitted in the cross-domain policy files. An exception to this rule is Content-Type headers. Content-Type headers are always allowed on cross-domain POST requests and do not need to be explicitly called in the cross-domain policy file. You can also enable additional headers by setting http-request-headers to:

  • A single header type as shown in the cross-domain policy file example (SOAPAction)

  • A comma separated list of headers (SOAPAction, x-custom-header)

  • The wildcard character to enable all headers (*)

The following table summarizes the header support for cross-domain requests.

HTTP Verb

Same-Domain calls

Cross-Domain calls

GET

Headers always allowed

Request headers are never allowed

POST

Headers always allowed

Request headers are allowed per cross-domain policy file

Path Restrictions for Cross-Domain Requests

In addition to requiring a cross-domain policy file, there are restrictions on the characters allowed in the path section of a cross-domain URI that is callable by a Silverlight-based application. The following illustration shows the path portion of a URI.

Path Portion of a URI

Path of a URI

The following characters are never allowed in a cross-domain request path.

  • Consecutive dot characters (..)

  • Dot followed by a forward slash (./)

  • Percent sign (%)

For more information about the format of cross-domain policy files and scenarios they enable, see Network Security Access Restrictions in Silverlight 2 and Making a Service Available Across Domain Boundaries.

Adobe Flash Cross-Domain Policy File

Silverlight supports a subset of the Adobe Flash cross-domain policy file (named crossdomain.xml). Silverlight supports the <allow-access-from> tag with the following attributes.

  • The domain attribute. The only value that is permitted for this attribute is "*", which means that the entire domain is exposed to all callers.

  • The secure attribute. This attribute is relevant only when the target domain is HTTPS. This attribute accepts a value of true or false, which indicates whether HTTP callers are allowed to access resources within this domain.

  • The headers attribute. This attribute lists accepted headers.

  • The following is an example of an Adobe Flash cross-domain policy file (crossdomain.xml) supported by Silverlight.

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM
"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*"/ headers="SOAPAction"
secure="true">
</cross-domain-policy>

Depending on the scenario, you can make HTTP calls by using a client-side proxy class or by constructing the calls yourself. The following section describes the different methods for making calls across a network by using HTTP.

Using Proxy Classes

You can create a proxy class from Web service metadata and use the proxy to communicate with a Web service from your Silverlight-based application. Silverlight uses Windows Communication Foundation (WCF) capabilities to create proxies and send SOAP 1.1 messages over HTTP. If you are using Visual Studio, you can right-click your Silverlight project and select Add Service Reference to have the proxy automatically created for you. The proxy creates the messages and handles the network communication for you. For more information about how to create Web Services to interact with Silverlight and how to generate proxies to use with existing Web services, see Building and Accessing Services using Generated Proxies. For more information about interacting with ADO.NET data Web services, see ADO.NET Data Services (Silverlight).

Creating HTTP Requests

If you want to make HTTP calls yourself, you can use the following classes found in the System.Net namespace.

These classes enable you to make GET and POST requests and allow request headers in some cases. In addition, you can configure these classes to enable progressive downloads on GET requests.

WebClient Class

The WebClient class provides a simple event-based model that enables you to download and upload streams and strings. The WebClient is a good choice if you do not want to use a proxy class. In general, this class is easy to use, but provides fewer options to customize messages that are sent across a network.

To make a POST request with WebClient and upload resource files or strings, you use one of the following methods.

You can set headers on POST requests by setting the WebClient..::.Headers property. Request headers must be allowed per the cross-domain policy file. For more information, see the Cross-Domain Communication section of this document.

The following code shows an example of making a POST request with WebClient.

Visual Basic
    ' Create the web client. 
Private client As New WebClient()
Public Sub New()
InitializeComponent()
' Associate the web client with a handler for its 
' UploadStringCompleted event. 
AddHandler client.UploadStringCompleted, AddressOf client_UploadStringCompleted
End Sub
Private Sub Button_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
' Create the request. 
Dim postRequest As String = "<entry xmlns='http://www.w3.org/2005/Atom'>" + _
"<title type='text'>New Restaurant</title>" + _
"<content type='xhtml'>" + _
" <div xmlns='http://www.w3.org/1999/xhtml'>" + _
" <p>There is a new Thai restaurant in town!</p>" + _
" <p>I ate there last night and it was <b>fabulous</b>.</p>" + _
" <p>Make sure and check it out!</p>" + _
" </div>" + " </content>" + _
" <author>" + _
" <name>Pilar Ackerman</name>" + _
" <email>packerman@contoso.com</email>" + _
" </author>" + _
"</entry>"
' Sent the request to the specified URL. 
client.UploadStringAsync(New Uri("http://blogs.contoso.com/post-create?blogID=1234", _
UriKind.Absolute), postRequest)
End Sub
' Event handler for the UploadStringCompleted event. 
Private Sub client_UploadStringCompleted(ByVal sender As Object, _
ByVal e As UploadStringCompletedEventArgs)
' Output the response. 
If e.[Error] IsNot Nothing Then
tb1.Text = e.[Error].Message
If tb1.Text = "" Then
tb1.Text = e.Error.InnerException.Message
End If
Else
tb1.Text = e.Result
End If
End Sub
    // Create the web client.
WebClient client = new WebClient();
public Page()
{
InitializeComponent();
// Associate the web client with a handler for its
// UploadStringCompleted event.
client.UploadStringCompleted +=
new UploadStringCompletedEventHandler(client_UploadStringCompleted);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// Create the request. 
string postRequest = "<entry xmlns='http://www.w3.org/2005/Atom'>"
+ "<title type='text'>New Restaurant</title>"
+ "<content type='xhtml'>"
+ "  <div xmlns='http://www.w3.org/1999/xhtml'>"
+ "   <p>There is a new Thai restaurant in town!</p>"
+ "   <p>I ate there last night and it was <b>fabulous</b>.</p>"
+ "   <p>Make sure and check it out!</p>"
+ "  </div>"
+ " </content>"
+ " <author>"
+ "   <name>Pilar Ackerman</name>"
+ "  <email>packerman@contoso.com</email>"
+ " </author>"
+ "</entry>";
// Sent the request to the specified URL.
client.UploadStringAsync(new                 Uri("http://blogs.contoso.com/post-create?blogID=1234",
UriKind.Absolute), postRequest);
}
// Event handler for the UploadStringCompleted event.
void client_UploadStringCompleted(object sender,
UploadStringCompletedEventArgs e)
{
// Output the response. 
if (e.Error != null)
tb1.Text = e.Error.Message;
else
tb1.Text = e.Result;
}

To make a GET request with WebClient to retrieve strings or other resource files, you use one of the following methods.

To enable progressive downloads for WebClient, set the AllowReadStreamBuffering property to false.

For more information about how to use WebClient to download content, see Downloading Content on Demand.

HttpWebRequest and HttpWebResponse Classes

The HttpWebRequest and HttpWebResponse classes support more complex communication scenarios than WebClient. You use HttpWebRequest and HttpWebResponse following the .NET Framework asynchronous pattern to make GET and POST requests. You use an IAsyncResult to provide a connection between the asynchronous request and response. HttpWebRequest delegates are always called on a new non-UI thread, which means that if you plan to use the response in the UI, you will have to invoke back to the UI thread. You can do this by retrieving the current SynchronizationContext.

The following code example demonstrates the pattern you must use for making a call using the HttpWebRequest and HttpWebResponse classes.

Visual Basic
    Private syncContext As SynchronizationContext
Private Sub Button_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
' Grab SynchronizationContext while on UI Thread 
syncContext = SynchronizationContext.Current
' Create request 
Dim request As HttpWebRequest = _
TryCast(WebRequest.Create(New Uri("http://blogs.contoso.com/post-create?blogID=1234", _
UriKind.Absolute)), HttpWebRequest)
request.Method = "POST"
' Make async call for request stream.
' Callback will be called on a background thread. 
Dim asyncResult As IAsyncResult = _
request.BeginGetRequestStream(New AsyncCallback(AddressOf RequestStreamCallback), _
request)
End Sub
Private statusString As String
Private Sub RequestStreamCallback(ByVal ar As IAsyncResult)
Dim request As HttpWebRequest = TryCast(ar.AsyncState, HttpWebRequest)
request.ContentType = "application/atom+xml"
Dim requestStream As Stream = request.EndGetRequestStream(ar)
Dim streamWriter As New StreamWriter(requestStream)
streamWriter.Write("<entry xmlns='http://www.w3.org/2005/Atom'>" + _
"<title type='text'>New Restaurant</title>" + "<content type='xhtml'>" + _
" <div xmlns='http://www.w3.org/1999/xhtml'>" + _
" <p>There is a new Thai restaurant in town!</p>" + _
" <p>I ate there last night and it was <b>fabulous</b>.</p>" + _
" <p>Make sure and check it out!</p>" + _
" </div>" + _
" </content>" + _
"<author>" + _
" <name>Pilar Ackerman</name>" + _
" <email>packerman@contoso.com</email>" + _
" </author>" + _
"</entry>")
' Close the stream.
streamWriter.Close()
' Make async call for response. Callback will be called on a background thread. 
request.BeginGetResponse(New AsyncCallback(AddressOf ResponseCallback), _
request)
End Sub
Private Sub ResponseCallback(ByVal ar As IAsyncResult)
Dim request As HttpWebRequest = TryCast(ar.AsyncState, HttpWebRequest)
Dim response As WebResponse = Nothing
Try
response = request.EndGetResponse(ar)
Catch we As WebException
statusString = we.Status.ToString()
Catch se As SecurityException
statusString = se.Message
If statusString = "" Then
statusString = se.InnerException.Message
End If
End Try
' Invoke onto UI thread 
syncContext.Post(AddressOf ExtractResponse, response)
End Sub
Private Sub ExtractResponse(ByVal state As Object)
Dim response As HttpWebResponse = TryCast(state, HttpWebResponse)
If response IsNot Nothing AndAlso response.StatusCode = HttpStatusCode.OK Then
Dim responseReader As New StreamReader(response.GetResponseStream())
tb1.Text = response.StatusCode.ToString() + " Response: " _
+ responseReader.ReadToEnd()
Else
tb1.Text = "Post failed: " + statusString
End If
End Sub
SynchronizationContext syncContext;
private void Button_Click(object sender, RoutedEventArgs e)
{
// Grab SynchronizationContext while on UI Thread   
syncContext = SynchronizationContext.Current;
// Create request   
HttpWebRequest request =
WebRequest.Create(new Uri("http://blogs.contoso.com/post-create?blogID=1234",
UriKind.Absolute))
as HttpWebRequest;
request.Method = "POST";
// Make async call for request stream.  Callback will be called on a background thread.  
IAsyncResult asyncResult =
request.BeginGetRequestStream(new AsyncCallback(RequestStreamCallback), request);
}
string statusString;
private void RequestStreamCallback(IAsyncResult ar)
{
HttpWebRequest request = ar.AsyncState as HttpWebRequest;
request.ContentType = "application/atom+xml";
Stream requestStream = request.EndGetRequestStream(ar);
StreamWriter streamWriter = new StreamWriter(requestStream);
streamWriter.Write("<entry xmlns='http://www.w3.org/2005/Atom'>"
+ "<title type='text'>New Restaurant</title>"
+ "<content type='xhtml'>"
+ "  <div xmlns='http://www.w3.org/1999/xhtml'>"
+ "   <p>There is a new Thai restaurant in town!</p>"
+ "   <p>I ate there last night and it was <b>fabulous</b>.</p>"
+ "   <p>Make sure and check it out!</p>"
+ "  </div>"
+ " </content>"
+ "<author>"
+ "   <name>Pilar Ackerman</name>"
+ "  <email>packerman@contoso.com</email>"
+ " </author>"
+ "</entry>");
// Close the stream.
streamWriter.Close();
// Make async call for response.  Callback will be called on a background thread.
request.BeginGetResponse(new AsyncCallback(ResponseCallback), request);
}
private void ResponseCallback(IAsyncResult ar)
{
HttpWebRequest request = ar.AsyncState as HttpWebRequest;
WebResponse response = null;
try
{
response = request.EndGetResponse(ar);
}
catch (WebException we)
{
statusString = we.Status.ToString();
}
catch (SecurityException se)
{
statusString = se.Message;
if (statusString == "")
statusString = se.InnerException.Message;
}
// Invoke onto UI thread  
syncContext.Post(ExtractResponse, response);
}
private void ExtractResponse(object state)
{
HttpWebResponse response = state as HttpWebResponse;
if (response != null && response.StatusCode == HttpStatusCode.OK)
{
StreamReader responseReader = new StreamReader(response.GetResponseStream());
tb1.Text = response.StatusCode.ToString() +
" Response: "  + responseReader.ReadToEnd();
}
else
tb1.Text = "Post failed: " + statusString;
}

You can send request headers, if they are allowed per the cross-domain policy file, by setting the HttpWebRequest..::.Headers property. For more information, see the Cross-Domain Communication section of this document.

To enable progressive downloads for HttpWebRequest, set the AllowReadStreamBuffering property to false. You can then make multiple calls to BeginRead on the response stream.

For more information about how to use HttpWebRequest and HttpWebResponse to communicate with a Web service, see How to: Make Requests to HTTP-Based Services.