WCF 服務同時支援 HTTP 和 HTTPS

摘要:用一個簡單範例說明如何讓 WCF 服務同時支援 HTTP 和 HTTPS。

工具:Visual Studio 2012 with Update 1

Step 1. 建立 WCF 服務

操作:
  1. 建立新專案,專案範本選擇 ASP.NET Empty Web Application,命名為 WcfHttpsDemo。
  2. 加入新的 WCF 服務,命名為 MyService.svc。預設範本會幫我們產生 IMyService.cs 和 MyService.cs,裡面有個 DoWork 方法。
此時的 web.config 內容會像這樣:

<configuration>
    <system.web>
      <compilation debug="true" targetFramework="4.5" />
      <httpRuntime targetFramework="4.5" />
    </system.web>
   
    <system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="">
                    <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
                    <serviceDebug includeExceptionDetailInFaults="false" />
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true"
            multipleSiteBindingsEnabled="true" />
    </system.serviceModel>
</configuration>

如果是 HTTP,這樣的預設組態就能用了,WCF 用戶端應用程式只要加入此服務參考,便可順利呼叫服務。但我們想要使用 HTTPS,而且是同時支援 HTTP 和 HTTPS,此預設組態當然就不夠用了。

要讓這個 WCF 服務接受 HTTP 和 HTTPS 請求,可在 web.config 的 <system.serviceModel> 區段裡面加上兩個 endpoint 元素和兩個 binding 元素,分別定義 HTTP 和 HTTPS 的端點與繫結參數。範例如下:

<services>
  <service name="WcfHttpsDemo.MyService">
    <endpoint address="" binding="basicHttpBinding" bindingConfiguration="HttpBinding" contract="WcfHttpsDemo.IMyService" />
    <endpoint address="" binding="basicHttpsBinding" bindingConfiguration="HttpsBinding" contract="WcfHttpsDemo.IMyService" />
  </service>
</services>
<bindings>
  <basicHttpBinding>
    <binding name="HttpBinding">
      <security mode="None">
        <transport clientCredentialType="None"></transport>
      </security>
    </binding>
  </basicHttpBinding>
  <basicHttpsBinding>
    <binding name="HttpsBinding">
      <security mode="Transport">
        <transport clientCredentialType="None"></transport>
      </security>
    </binding>
  </basicHttpsBinding>
</bindings>

注意兩個 binding 參數的差異:HttpBinding 的安全模式(security 元素的 model 屬性)為 "None",而 HttpsBinding 的安全模式為 "Transport"。如果將兩個 binding 的安全模式設為相同,例如二者皆為 "Transport",在存取服務時會出現錯誤訊息:

A binding instance has already been associated to listen URI 'https://michael-a43s/MyService.svc'. If two endpoints want to share the same ListenUri, they must also share the same binding object instance. The two conflicting endpoints were either specified in AddServiceEndpoint() calls, in a config file, or a combination of AddServiceEndpoint() and config.

還有一個要注意的地方,是當我們使用 IIS Express 作為開發時期的伺服器時,必須把 ASP.NET 應用程式專案的 SSL Enabled 屬性改為 True。你可以在 Solution Explorer 中點選專案名稱,然後到屬性視窗中設定 SSL Enabled 屬性。參考下圖:


少了此動作,在 web.config 中加入前述組態(支援 HTTP 和 HTTPS)之後,用瀏覽器存取 WSDL 時就會出現錯誤訊息:

Could not find a base address that matches scheme https for the endpoint with binding BasicHttpsBinding. Registered base address schemes are [http].

Step 2. 部署 WCF 服務

部署到本機 IIS,站台名稱取名 WcfDemo,繫結 80 和 443 port。如下圖:

在建立 HTTPS 繫結時必須指定 SSL 憑證。在此範例中,我用的是 IIS Express 開發憑證:


試以瀏覽器查看 WSDL 文件,網址為 http://localhost/MyService.svc?wsdl。結果應該會先看到如下圖的警告:


選擇「繼續瀏覽此網站」之後,就會顯示 WSDL。注意裡面有兩個 <wsdl:port> 元素,可以看出 MyService 同時支援 HTTP 和 HTTPS:



Step 3:撰寫 WCF 用戶端程式

操作:
  1. 建立一個 Windows Forms 應用程式專案。
  2. 加入服務參考,位址輸入 http://localhost/MyService.svc。命名空間指定為 "Demo"。
接著修改 app.config 內容,如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="BasicHttpBinding_IMyService">
          <security mode="None">
            <transport clientCredentialType="None"></transport>
          </security>
        </binding>
      </basicHttpBinding>
      <basicHttpsBinding>
        <binding name="BasicHttpsBinding_IMyService">
          <security mode="Transport">
            <transport clientCredentialType="None"></transport>
          </security>
        </binding>
      </basicHttpsBinding>
    </bindings>
    <client>
      <endpoint name="BasicHttpBinding_IMyService" contract="Demo.IMyService"
                address="http://michael-a43s/MyService.svc"
                binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_IMyService"  />
      <endpoint name="SecureHttpBinding_IMyService" contract="Demo.IMyService"
                address="https://michael-a43s/MyService.svc"
                binding="basicHttpsBinding"
                bindingConfiguration="BasicHttpsBinding_IMyService" />
    </client>
  </system.serviceModel>
</configuration>

與伺服器端的 web.config 類似,此用戶端應用程式組態檔中也有兩組 binding 和 endpoint,分別設定 HTTP 和 HTTPS 的參數。不同的地方是這裡的兩個 endpoint 元素都有指定 name 參數,以便程式在建立 proxy 物件時能夠明確指定使用哪一個 endpoint。

為了方便測試,我在 form 上面放一個 ComboBox,並預先把兩個 endpoint 的名稱塞給 ComboBox 的 Items 集合,參考下圖:


接著撰寫按鈕 "Call MySevice" 點擊事件的處理常式:

private void button1_Click(object sender, EventArgs e)
{
    var client = new Demo.MyServiceClient(comboBox1.Text);
    string s = client.DoWork("Michael");
    MessageBox.Show(s);
}

執行看看。首先選擇 BasicHttpBinding_IMyService,這應該沒問題。接著改用 SecureHttpBinding_IMyService,結果按下 "Call MyService" 按鈕之後卻出現錯誤:

無法利用授權 'michael-a43s' 為 SSL/TLS 安全通道建立信任關係。

英文訊息是 "Could not establish trust relationship for the SSL/TLS secure channel with authority 'michael-a43s'"。其中 'michael-a43s' 是我的本機電腦名稱。

這是因為我在伺服器端使用的 SSL 憑證是 IIS Express 開發版憑證的緣故。試過將 IIS Express 開發憑證從個人憑證區搬到「受信任的根憑證授信單位」之下,還是出現同樣錯誤。

碰到這個狀況,一個暫時解法是強迫應用程式忽略憑證是否有效,範例如下:

private void Form1_Load(object sender, EventArgs e)
{
    ServicePointManager.ServerCertificateValidationCallback = (
        object s,
        System.Security.Cryptography.X509Certificates.X509Certificate certificate,
        System.Security.Cryptography.X509Certificates.X509Chain chain,
        System.Net.Security.SslPolicyErrors sslPolicyErrors) >=
    {
         if (certificate.Subject.Contains("localhost"))
         {
             return true;
         }
         return false;
    };
}

這段程式碼的作用是當 ServicePointManager 檢查憑證時,若憑證主體(subject)名稱包含 "localhost" 字樣,就一律放行。註:IIS Express 開發憑證的主體名稱是 "localhost"。

正式環境通常會安裝有效的 SSL 憑證,應該就不會用到此臨時解法吧。

Trouble Shooting

有一次,我將我的 ASP.NET 應用程式部署到第二台機器之後,存取該應用程式中的 WCF 服務時出現錯誤:

Could not find a base address that matches scheme http for the endpoint with binding BasicHttpBinding. Registered base address schemes are [https].

由於另一台機器也有部署相同的應用程式,程式碼和組態檔完全相同。經過比對之後,發現兩個網站唯一的差別是第二台機器上的網站沒有設定 HTTPS 繫結。加上 HTTPS 繫結並指定一個 SSL 憑證之後,問題便消失了。

延伸閱讀

沒有留言:

技術提供:Blogger.
回頂端⬆️