Cross-domain calls for Silverlight apps on self-hosted service

最近開始將一些新的App改用WPF來實作,而在撰寫的過程中也一起試了一下Silverlight,畢竟如果用SL可以搞定的話,Web部屬的方便性還是要比Client-Server一台一台去安裝要來的方便很多的。

而在測試Silverlight的過程中,想起在之前就有看過一些安全性的規定,在跨網域(cross-domain)的狀況下,Silverlight是會被限制存取的;而現在大部分的App後面都帶著一隻Windows Service,並且裝載了WCF Service提供呼叫,所以就優先來測試一下這個部分了。

最近開始將一些新的App改用WPF來實作,而在撰寫的過程中也一起試了一下Silverlight,畢竟如果用SL可以搞定的話,Web部屬的方便性還是要比Client-Server一台一台去安裝要來的方便很多的。
而在測試Silverlight的過程中,想起在之前就有看過一些安全性的規定,在跨網域(cross-domain)的狀況下,Silverlight是會被限制存取的;而現在大部分的App後面都帶著一隻Windows Service,並且裝載了WCF Service提供呼叫,所以就優先來測試一下這個部分了。

在進入今天的正題之前,先來看一下在一般的情形下(這裡的一般指的是裝載在IIS上的Service或是網站資源等狀況),Silverlight apps如果需要跨網域去存取資源的話要怎麼做呢? 首先先說一下跨網域,跨網域的意思是什麼呢?比如說Silverlight裝載的位置是http://www.myweb.com/myApp,而開始執行App之後,假設App需要去呼叫位於http://www.othersite.com/xxxx.asmx所提供的Web Service,那麼這個時候的動作就是cross-domain了,那怎麼去處理這個問題呢?首先可以參考一下這篇資料
在上面這篇文章可以看到有兩種方式可以處理,加入『clientaccesspolicy.xml』或是『crossdomain.xml』都可以,首先要特別注意的是不論使用哪個檔案,都必須放置在服務的提供端,而不是放在Silverlight app這邊,所以如果提供服務的這端如果不是你自己可以控制的,那麼就要聯絡服務端,確認是不是可以開放呼叫的權限,要不就是要另外自行撰寫另外的Service來做中介,一邊向原始來源取得資料,另一邊提供給Silverlight app使用了。 這邊簡單的看一下clientaccesspolicy.xml的內容,檔案內容會長的像是下面這樣

  
    
      
        
      
      
        
      
    
  
內容還滿容易理解的,在allow-form http-request-headers="SOAPAction"這個是表示允許SOAP的操作要求,而接下來的domain uri="*"在這邊是指允許所有地方連線過來取得資源的要求,而要限定只允許某個Uri的話就會是設定成像domain uri="http://www.othersite.com"這樣的設定,詳細的說明可以參考一下上面提到的網頁內容。
檔案準備好了之後,是要擺放在服務端的根目錄中;以上面提到的例子來說,服務是位於http://www.othersite.com/xxxx.asmx,那麼檔案就必須在http://www.othersite.com/clientaccesspolicy.xml,如果位置錯誤的話,那麼Silverlight app呼叫的時候也是會跟著出現cross-domain的錯誤的,這個要特別注意一下。

好,那麼接下來就進入正題了,在Self-hosted web service(自我裝載的web服務)的情形要怎麼處理這個問題;那麼也許你心中的第一個想法跟我是一樣的,『不是就是把上面的檔案給放到網站的根目錄就行了』,結果我就這麼給他用下去,結果就..殘念,這個想法是行不通的,為什麼?
以我的應用情境來說,我是建立了一個Windows Service,在這個Windows Service中利用ServiceHost類別提供的功能,來裝載進一個Web Service給其他的用戶端呼叫使用,而通常這樣的狀況下,會定義出一個服務的名稱(Uri)以及使用的連接埠號碼,例如說http://192.168.1.1:1234/myService這樣的服務位置,看過上面的說明,很直覺的就想要把設定檔案放到http://192.168.1.1/clientaccesspolicy.xml這個位置,但是由於服務並非裝載在IIS上,因此Silverlight app發出要求的時候是會到http://192.168.1.1:1234/clientaccesspolicy.xml這個地方來確認是不是有相關的存取權限的,因為找不到檔案,所以Silverlight也無法順利取得資源。那麼解決方式也就出來了,就是要想辦法搞出http://192.168.1.1:1234/clientaccesspolicy.xml這個東西出來,來讓Silverlight app可以順利的取得存取的權限,那就搞定了,那麼接下來就是要修改一下Service這邊的程式碼了。

首先,下面先列出參考來源,我也是依照下面這些資源的方式下去修改程式碼、測試的
接下來看一下要修改的部分,首先在Widnwos Service的架構中,通常會有下面這些檔案(以Visual Basic為例,檔案名稱為示範用途不會一致)
  • myWindwosService.vb
  • 這是Widnwos Service主要程式碼的檔案
  • ImyWebService.vb
  • 這個檔案是定義服務提供的方法、資料型別/內容的介面(interface)檔案
  • myWebServiceHost.vb
  • 這個檔案是實作介面的類別,介面的細節都會在這個程式碼中加以實作
接下來,由於要實作產生cross-domain時的xml檔案動作,先新增一個介面(interface)檔案,這邊名稱就取做IPolicyRetriever.vb,內容會像是下面這樣

Imports System.ServiceModel
Imports System.ServiceModel.Web


Public Interface IPolicyRetriever
    
    Function GetSilverlightPolicy() As Stream
End Interface
很簡單的一個檔案;其中這個GetSilverlightPolicy的方法就是要實作當Silverlight app對http://192.168.1.1:1234/clientaccesspolicy.xml發出要求的時候,需要處理的動作了。接下來在myWebServiceHost.vb這個原本實作Web Service提拱的功能的檔案中,再實作一個剛剛定義出來的介面(IPolicyRetriever),實作的程式碼節錄出來大致會像是下面這邊

    Implements ImyWebService
    Implements IPolicyRetriever

    Public Function GetSilverlightPolicy() As System.IO.Stream Implements IPolicyRetriever.GetSilverlightPolicy
        ServiceModel.Web.WebOperationContext.Current.OutgoingResponse.ContentType = "application/xml"
        Return New IO.MemoryStream(Encoding.UTF8.GetBytes(PolicyXML))
    End Function

    Private Const PolicyXML As String =
        "" &
        "" &
            "" &
                "" &
                  "" &
                      "" &
                  "" &
                  "" &
                      "" &
                  "" &
                "" &
            "" &
        ""
End Class
這邊我是有點偷懶,直接宣告了一個字串,裡面就是xml檔案內容,存取權限的部分也沒有做任何的限制,這個部分可以依照實際的需求再去調整。
實作完介面的方法之後,接下來就是要設定一下ServiceHost的部分了;在myWindowsService.vb這個檔案中,應該會看到原本設定ServiceHost的程式碼區域,以我的使用情況來說,會像是下面這樣

        basic.MaxBufferSize = 8192000
        basic.MaxBufferPoolSize = 8192000
        basic.MaxReceivedMessageSize = 8192000
        basic.MessageEncoding = WSMessageEncoding.Text

        Host = New ServiceHost(GetType(AcsServiceHost), New Uri("http://192.168.1.1:1234/myWindowsService"))
        Host.AddServiceEndpoint(GetType(IAcsService), basic, "http://192.168.1.1:1234/myWindowsService")

        Host.AddServiceEndpoint(GetType(IPolicyRetriever), New WebHttpBinding, "http://192.168.1.1:1234").Behaviors.Add(New Description.WebHttpBehavior)
        Dim smb As Description.ServiceMetadataBehavior = New Description.ServiceMetadataBehavior
        smb.HttpGetEnabled = True
        Host.Description.Behaviors.Add(smb)

        Host.Open()
其中可以看到對於Host(也就是ServiceHost類別的執行個體)加入了兩個Endpoint,第一個是原先使用中的,是屬於basicHttpbinding的類型,而第二個Endpoint是WebHttpBinding,第二個加入的這個主要就是當要求cross-domain需要的xml檔案時,要連接到的網址用的;而往下一點的程式碼也可以看到,必須要把httpGetEnable給設定為True才可以正常動作喔。
這樣就完成了整個動作,如果想要測試看看是不是能順利的讓Silverlight app呼叫的話,在直接測試app之前,可以先用IE連線到http://192.168.1.1:1234/clientaccesspolicy.xml來看看是不是有正常的產生出xml資料,有的話Silverlight app就可以正常運作了。
那麼就趕緊動手測試看看吧~