用WSE在Web服務中驗證用戶身份

一、Web服務安全與WS-Security   毫無疑問,SOAP和XML Web服務在交互操作和標準上已經完全改變了電子商務領域的格局。   然而直到最近,在Web服務技術領域仍然存在着一些缺陷,那就是處理消息級別的安全、認證、 加密 、數字簽名、路由和附件等問題的能力。爲了解決這些安全問題,像IBM、 Microsoft 和Verisign這樣的公司和組織正牽頭合作制定統一的Web服務安全規範,以便利用它們原有的Web服務交互操作概念和商業模型,他們推出了WS-Security等規範。可以這麼說,自從SOAP規範形成以後,WS-Security規範及其後續的工作可能是Web服務技術領域的一次最重要的進步。   隨着WS-Security規範的定稿,各大軟件廠商開始認真地考慮爲其產品提供使用相同Web服務安全語言的接口和 編程 工具箱,Web服務開發者也將能夠使用這些廠商提供的工具加強他們所開發的Web服務的安全性。    二、 WSE 安全性能簡介    Microsoft 推出了Web Services Enhancements 1.0 for .NET (以下簡稱 WSE ),它是一個類庫,用於實現高級 Web 服務協議,這也是該公司的第一個使用WS-Security等規範實現SOAP消息安全的工具套件。   保護Web服務安全的一個很重要的環節就是保護其SOAP消息傳遞的安全。   使用 WSE 後,SOAP消息可以自己驗證其完整性,並可使用定義在WS-Security規範中的機制 加密 。    WSE 1.0支持的所有WS-Security特性都是通過實現SecurityInputFilter和 SecurityOutputFilter對象的安全性輸入輸出過濾器實現的,它支持的安全特性有:   1. 數字簽名   2. 加密   3. 使用用戶名令牌簽名並 加密   4. 使用X.509證書籤名並 加密   5. 使用自定義二進制令牌簽名並 加密    WSE 1.0不支持Security Assertion Markup Language(SAML,安全聲明標註語言),但 Microsoft 公司正積極在其 .NET Server中實現SAML體系結構。當然,開發者自己可以自由的實現SAML。唯一的不足是還不能使用WSDL描述遵循WS-Security規範的Web服務的WS-Security接口。    WSE 的體系結構模型基於處理入站和出站SOAP消息的過濾器管道。它是建立在已有的SOAPExtension類的基礎上的,有使用過SOAPExtension類行進壓縮、 加密 、記錄和其它操作經驗的開發者會發現他們對 WSE 其實很熟悉。    WSE 提供了一個 Microsoft .Web.Services.SoapContext類,讓我們可以處理WS-Security SOAP頭和其它入站的SOAP消息頭,同時可爲出站的SOAP消息添加WS-Security頭。 WSE 還有一個包裝類爲SOAP請求和響應添加SOAPContext(與HttpContext類似),同時服務器使用一個SOAPExtension類“ Microsoft .Web.Services.WebServicesExtension”,讓我們可以驗證入站的SOAP消息,還提供了我們可從我們的WebMethod中訪問的請求和響應SoapContext。   學習使用 WSE 最大的障礙在於有時很難理解 Microsoft 的技術文檔和相關文章,即使對於那些有豐富經驗的高級開發人員來說也是如此,並且關於這方面的文章很少。在本文中,我將給出一個簡單的例子,介紹如何使用 WSE 實現基本的用戶名令牌的驗證過程,以保證Web服務的安全。    三、設置 WSE 環境   爲了設置基本的 WSE 環境,我們需要配置 ASP .NET 應用程序,使其能夠使用 WSE SOAPExtension。最簡單的方法是把所需的/configuration/system.web/webServices/soapExtensionTypes/Add元素添加到你的Web服務虛擬目錄中的web.config裏,如下所示: <webServices> <soapExtensionTypes> <add type=" Microsoft .Web.Services.WebServicesExtension, Microsoft .Web.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" priority="1" group="0" /> </soapExtensionTypes> </webServices>   注意type屬性必須寫在一行中,但是在文中考慮到篇幅的問題需要把它分爲幾行,所以請讀者多加註意。而且要注意,在開始使用 WSE 之前,我們必須在工程中加入對 Microsoft .Web.Services.dll的引用。    四、基本的用戶名令牌認證   在我們數字簽名SOAP消息之前,必須先弄清楚誰正在簽名。因此,我們將探討一下用戶名令牌(UsernameToken)的概念,同時瞭解 WSE 如何允許我們驗證用戶名令牌。   爲了在Web服務中使用 WSE 驗證用戶名/密碼,我們需要知道 WSE 在這方面爲我們提供了什麼?WS-Security定義了一個UsernameToken元素,它提供了基本用戶名/密碼驗證的方法。如果你有使用HTTP的經驗,那麼你會發現UsernameToken與Basic Authentication非常類似。有三種用戶名令牌,但是通常情況下我們只對最後兩種最感興趣: <!--明文密碼--> <UsernameToken> <Username>user1</Username> <Password Type="wsse:PasswordText">suangywang</Password> </UsernameToken>   這種方法使用明文密碼。我們不難想象,在服務器上將進行覈對數據庫,驗證用戶名與密碼,看是否有匹配的用戶名/密碼對這一系列驗證操作。 <!--密碼摘要--> <UsernameToken> <Username>user1</Username> <Password Type="wsse:PasswordDigest"> QSMAKo67+vzYnU9TcMSqOFXy14U= </Password> </UsernameToken>   這種方法發送一個密碼摘要(digest)代替明文密碼。使用密碼摘要,密碼就不會通過網絡發送,這樣 黑客 就不太可能算出Web服務的密碼。密碼摘要是用散列函數計算的。這個過程只是單向的,意味着將函數反向並找到對應於摘要的消息是不可能的,因爲該過程以這樣一種方式實現,所以找到散列到同一摘要的兩條不同密碼在計算上難以實現。但是 黑客 可以發送散列密碼,然後冒充原始發送人被驗證。爲了避免這個問題,Web Services Security Addendum(Web服務安全補遺)已經增加一個輔助的保護措施。補遺中規定必須發送密碼的摘要版本,而不僅僅發送散列密碼。這個摘要信息包含一個密碼散列,標識請求的唯一的Nonce和創建時間。因此絕對不會出現相同的兩個密碼散列。如下所示是修正後的用戶名令牌UsernameToken。 <!--修正後的用戶名令牌--> <wsse:UsernameToken xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" wsu:Id="SecurityToken-59845323-5dcb-4a6b-a7fb-94a0d7357a20"> <wsse:Username>User1</wsse:Username> <wsse:Password Type="wsse:PasswordDigest"> gpBDXjx79eutcXdtlULIlcrSiRs= </wsse:Password> <wsse:Nonce> h52sI9pKV0BVRPUolQC7Cg== </wsse:Nonce> <wsu:Created>2003-6-20T21:16:50Z</wsu:Created> </wsse:UsernameToken>   雖然每個合法請求都有一個不同的散列,但是你也必須防止惡意用戶把其他用戶的合法請求中的整個UsernameToken拿出放入自己的非法請求中。你可以使用Timestamp(時間戳標頭)來最小化這種危險。時間戳標頭用來表示消息的創建時間和過期時間,指明消息的週期以及何時可以認爲該消息失效。 例如,你可能想指定消息在40秒以後失效,並且超過40秒服務器就不會接收UsernameToken。但是機器之間的時鐘同步問題可能會造成有效的請求被拒絕的情況。所以使用時間戳也並不是一個盡善盡美的解決方法。爲了解決這個問題,Web服務可以保存一張最近收到的UsernameToken的Nonce值的表,如果收到的一個請求的Nonce值已經被使用了,那麼就絕對不會接受這個請求。如果你接收幾個使用相同Nonce的請求,那麼你要考慮把這幾條請求全部丟棄,因爲很有可能先到的請求是非法請求。還要瞭解到Nonce覈對技術並不能防止惡意用戶截獲合法的輸入信息,並把原始信息中的UsernameToken加入自己的消息,然後發送到目的地。這時就需要爲消息添加數字簽名或安全證書,以保護其不受攻擊。數字簽名和安全證書的相關知識在本文中不會涉及,請讀者查閱相應文獻。   所有的散列保護都需要消息發送端和接收端知道用戶的密碼。在客戶端,人們期望系統能夠提示用戶輸入密碼。而在服務器端,需要保存帶有有效用戶名/密碼對的表,以供系統查找。我們下面將介紹 WSE 如何使用一個Password Provider(密碼提供者)機制來解決這兩個問題。    五、IPasswordProvider接口    WSE 定義了一個 Microsoft .Web.Services.Security.IPasswordProvider接口類,我們必須實現這個類來註冊一個密碼提供者。這個接口有一個方法GetPassword,它接收一個 Microsoft .Web.Services.Security.UsernameToken作爲輸入參數,該方法返回指定用戶的密碼。其思想是你可以使用任何你想用的機制保存有效的用戶名/密碼對,然後提供了一個實現IPasswordProvider接口的類,來讓 WSE 訪問你的特定密碼存儲機制。你甚至可以執行你自己的UsernameToken的摘要(Digest)和散列(Hash)的組合,甚至使用一個共享的密碼,以進一步控制你的認證基礎結構。   爲了把你特定的Password Provider(密碼提供者)告訴 WSE ,你必須配置合適的 WSE 設置。首先要添加一個 Microsoft .Web.Services元素到應用程序的配置文件中的配置元素中。還要指定可以讀懂特定配置信息的 WSE 類。可以把下面的configSections添加到機器上的Machine.config或單獨的

 

[NextPage]  

 

 

[NextPage]

 

ctions> <section name="microsoft.web.services" type=" Microsoft .Web.Services.Configuration.WebServicesConfiguration, Microsoft .Web.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </configSections>   在本例中,我們將使用Northwind數據庫Employees表的一個修改版本來進行查詢任務。因爲PasswordProvider接口需要返回一個與UsernameToken對象的密碼部分匹配的實際密碼,所以通常,我們只需要使用 WSE 加密 我們的用戶名和密碼,然後再通過網絡傳送給Web服務。   如果你在Solution Explorer中選中你的工程並在其上點擊右鍵,你將看到在底部增加了一個新的菜單“ WSE Settings”,你可以在其中設置所有重要的配置和其它使用 WSE 的配置:   這可讓我們很容易的設置Password Provider Implementation(密碼提供者實現)元素,Decryption Key Provider Implementation(解密鑰提供者)元素,X.509 Certificate(X.509 證書)設置,甚至是我們希望使用的Binary Security Tokens(二進制安全令牌)。此外,其他的選項卡還可以配置用於 WSE 管道的輸入輸出過濾器,配置路由,啓動診斷功能等等。雖然它不能做我們想做的每件事,但這是 WSE 易用化的一個良好的開端。   PasswordProvider安全元素是web.config中的<configuration>父元素的一個子元素,它告訴 WSE 你使用哪個類來實現PasswordProvider接口: <microsoft.web.services>  <security>   <!-- NAMESPACE . CLASSNAME , ASSEMBLYNAME -->   <passwordProvider type=" WSE Security. WSE PasswordProvider, WSE Security" />  </security> </microsoft.web.services>   讓我們看看在本例中如何實現它: namespace WSE Security {  public class WSE PasswordProvider : IPasswordProvider  {   public string GetPassword(UsernameToken token)   {    try    {     SqlConnection cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());     cn.Open();     SqlCommand cmd = new SqlCommand("SELECT Username, password from Employees where username =" + token.Username + "",cn);     SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);     dr.Read();     return dr["password"].ToString();    }    catch(Exception ex)    {     throw new Exception (ex.Message);    }   }  } }   上面我們給出的代碼可以完全實現IPasswordProvider接口,通過用戶名/密碼來驗證一個用戶,當然了,還可以把它做得更復雜一些,這請讀者們自己去完成。實際上,我們在 編程 的過程中基本沒有寫太多用戶驗證的代碼,大部分工作都由 WSE 暗中處理了。    六、編寫一個使用WS-Security的WebMethod   現在我們需要創建一個使用WS-Security的WebMethod。 這裏,我實現了一個簡單的方法,它運行Northwind數據庫的CustOrderHist存儲過程,接收一個字串UserID作爲唯一的參數,並返回一個DataSet。如果調用Web服務的客戶端可以通過消息級UsernameToken驗證,那麼就可以取回DataSet。如果不能通過驗證的話,客戶端將得到一個異常,告知它不能通過驗證。 WSE 的優點在於你只要付出一點點勞動就可以了,大部分的工作已經由 WSE 在暗中爲你完成了,所以你可以把大部分時間花費在構建Web服務的內容上,而不是爲了構建一個安全的Web服務機制而疲於奔命。 [WebMethod] public DataSet CustOrderHist(string CustId) {  // 只接受 SOAP格式的請求  SoapContext requestContext = HttpSoapContext.RequestContext;  if(requestContext==null)  {   throw new ApplicationException("Non-SOAP request!");  }  bool valid=false;  try  {   foreach(SecurityToken tkn in requestContext.Security.Tokens)   {    if(tkn is UsernameToken)    valid=true;   }  }  catch(Exception ex)  {   throw new Exception( ex.Message + ": " + ex.InnerException.Message);  }  if (valid==false)   throw new ApplicationException("Invalid or Missing Security Token.");  SqlConnection cn;  SqlDataAdapter da;  DataSet ds;  cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());  cn.Open();  da = new SqlDataAdapter("custorderHist " +CustId + "", cn);  ds = new DataSet();  da.Fill(ds, "CustOrderHist");  return ds; }   使用上面的WebMethod,我們就可在服務器上實現驗證用戶名/密碼的操作。WebMethod必須引用 Microsoft .Web.Services和 Microsoft .Web.Services.Security域名空間。現在,我們要構建一個 ASP .NET 客戶端,這個客戶端能夠發送驗證所需的SOAP頭,並可調用我們的Web服務方法。    七、構建 WSE ASP .NET 客戶端   對於客戶端, WSE 提供了繼承於System.Web.Services.Protocols.SoapHttpClientProtocol類的 Microsoft .Web.Services.WebServicesClientProtocol類。當你在 Visual Studio.NET 中選擇“Add Web Reference”選項的時候,或者使用WSDL.exe程序創建基於WSDL的客戶端代碼時,你需要使用SoapHttpClientProtocol類。你可做的就是使用 Visual Studio.NET 中的“Add Web Reference”選項或者WSDL.exe程序爲你的客戶端生成代理類,然後把代理類從繼承於SoapHttpClientProtocol改爲繼承於WebServicesClientProtocol。這樣代理類就有了RequestSoapContext和ResponseSoapContext屬性,你可以使用它們訪問你發送或接收的WS-Security頭。在 C# 工程中,如果你已經使用了“Add Web Reference”選項,你可以點擊Solution Explorer中的“Show All Files”按鈕,點擊這個按鈕就可在Solution Explorer的Web References結點中顯示Reference.cs文件,讓你可以編輯這個文件。   爲了創建正確的UsernameToken和在消息級調用Web服務的代理方法,需要使用下面的代碼: private void Button1_Click(object sender, System.EventArgs e) {  localhost.SecurityService Wse wse =new localhost.SecurityService Wse ();  UsernameToken tkn = new UsernameToken(txtUsername.Text,txtPassword.Text,PasswordOption.SendHashed);   wse .RequestSoapContext.Security.Tokens.Add (tkn);  try  {   DataSet ds= wse .CustOrderHist(txtCustID.Text);   DataGrid1.DataSource=ds;   DataGrid1.DataBind();  }  catch(Exception ex)  {   DataGrid1.Visible=false;   lblMessages.Text=ex.Message;  } }   我們要做的就是從客戶端的兩個文本輸入框txtUsername和txtPassword中取得輸入字串,然後使用PasswordOption.SendHashed把它們結合起來創建一個有效的UserNameToken。當調用Web服務時, WSE SOAP擴展驗證請求的一般格式,然後覈對密碼散列並從我們的PasswordProvider方法中取得的密碼。如果兩者匹配,我們就可調用Web服務方法,客戶端返回數據集,顯示在一個網格中。   我們現在已經創建了一個完整的使用 WSE 配合數據庫驗證SHA1摘要散列用戶名/密碼的Web服務,希望讀者們能通過本文瞭解到使用 WSE 保證Web服務安全的基本措施和方法,並能在實際工作中合理的去應用。   在文章的最後,我們給出修改Northwind數據庫Employees表的SQL腳本,給這個表添加了所需的username和password列,同時在這個表中插

 

[NextPage]  

 

 

[NextPage]

 

了一條新紀錄,其Firstname、Lastname、Username、Password和roles字段分別爲“User”,“One”,“user1”,“pass1”和“user”。 USE NORTHWIND GO ALTER TABLE [dbo]. ADD [Username] [varchar] (100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [Password] [varchar] (100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [roles] [varchar] (250) COLLATE SQL_Latin1_General_CP1_CI_AS NULL GO INSERT INTO EMPLOYEES (Firstname, Lastname,Username, [Password], roles) VALUES(User,One, user1, pass1, user) GO

 

  

 

 

[NextPage]

 

ctions> <section name="microsoft.web.services" type=" Microsoft .Web.Services.Configuration.WebServicesConfiguration, Microsoft .Web.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </configSections>   在本例中,我們將使用Northwind數據庫Employees表的一個修改版本來進行查詢任務。因爲PasswordProvider接口需要返回一個與UsernameToken對象的密碼部分匹配的實際密碼,所以通常,我們只需要使用 WSE 加密 我們的用戶名和密碼,然後再通過網絡傳送給Web服務。   如果你在Solution Explorer中選中你的工程並在其上點擊右鍵,你將看到在底部增加了一個新的菜單“ WSE Settings”,你可以在其中設置所有重要的配置和其它使用 WSE 的配置:   這可讓我們很容易的設置Password Provider Implementation(密碼提供者實現)元素,Decryption Key Provider Implementation(解密鑰提供者)元素,X.509 Certificate(X.509 證書)設置,甚至是我們希望使用的Binary Security Tokens(二進制安全令牌)。此外,其他的選項卡還可以配置用於 WSE 管道的輸入輸出過濾器,配置路由,啓動診斷功能等等。雖然它不能做我們想做的每件事,但這是 WSE 易用化的一個良好的開端。   PasswordProvider安全元素是web.config中的<configuration>父元素的一個子元素,它告訴 WSE 你使用哪個類來實現PasswordProvider接口: <microsoft.web.services>  <security>   <!-- NAMESPACE . CLASSNAME , ASSEMBLYNAME -->   <passwordProvider type=" WSE Security. WSE PasswordProvider, WSE Security" />  </security> </microsoft.web.services>   讓我們看看在本例中如何實現它: namespace WSE Security {  public class WSE PasswordProvider : IPasswordProvider  {   public string GetPassword(UsernameToken token)   {    try    {     SqlConnection cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());     cn.Open();     SqlCommand cmd = new SqlCommand("SELECT Username, password from Employees where username =" + token.Username + "",cn);     SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);     dr.Read();     return dr["password"].ToString();    }    catch(Exception ex)    {     throw new Exception (ex.Message);    }   }  } }   上面我們給出的代碼可以完全實現IPasswordProvider接口,通過用戶名/密碼來驗證一個用戶,當然了,還可以把它做得更復雜一些,這請讀者們自己去完成。實際上,我們在 編程 的過程中基本沒有寫太多用戶驗證的代碼,大部分工作都由 WSE 暗中處理了。    六、編寫一個使用WS-Security的WebMethod   現在我們需要創建一個使用WS-Security的WebMethod。 這裏,我實現了一個簡單的方法,它運行Northwind數據庫的CustOrderHist存儲過程,接收一個字串UserID作爲唯一的參數,並返回一個DataSet。如果調用Web服務的客戶端可以通過消息級UsernameToken驗證,那麼就可以取回DataSet。如果不能通過驗證的話,客戶端將得到一個異常,告知它不能通過驗證。 WSE 的優點在於你只要付出一點點勞動就可以了,大部分的工作已經由 WSE 在暗中爲你完成了,所以你可以把大部分時間花費在構建Web服務的內容上,而不是爲了構建一個安全的Web服務機制而疲於奔命。 [WebMethod] public DataSet CustOrderHist(string CustId) {  // 只接受 SOAP格式的請求  SoapContext requestContext = HttpSoapContext.RequestContext;  if(requestContext==null)  {   throw new ApplicationException("Non-SOAP request!");  }  bool valid=false;  try  {   foreach(SecurityToken tkn in requestContext.Security.Tokens)   {    if(tkn is UsernameToken)    valid=true;   }  }  catch(Exception ex)  {   throw new Exception( ex.Message + ": " + ex.InnerException.Message);  }  if (valid==false)   throw new ApplicationException("Invalid or Missing Security Token.");  SqlConnection cn;  SqlDataAdapter da;  DataSet ds;  cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());  cn.Open();  da = new SqlDataAdapter("custorderHist " +CustId + "", cn);  ds = new DataSet();  da.Fill(ds, "CustOrderHist");  return ds; }   使用上面的WebMethod,我們就可在服務器上實現驗證用戶名/密碼的操作。WebMethod必須引用 Microsoft .Web.Services和 Microsoft .Web.Services.Security域名空間。現在,我們要構建一個 ASP .NET 客戶端,這個客戶端能夠發送驗證所需的SOAP頭,並可調用我們的Web服務方法。    七、構建 WSE ASP .NET 客戶端   對於客戶端, WSE 提供了繼承於System.Web.Services.Protocols.SoapHttpClientProtocol類的 Microsoft .Web.Services.WebServicesClientProtocol類。當你在 Visual Studio.NET 中選擇“Add Web Reference”選項的時候,或者使用WSDL.exe程序創建基於WSDL的客戶端代碼時,你需要使用SoapHttpClientProtocol類。你可做的就是使用 Visual Studio.NET 中的“Add Web Reference”選項或者WSDL.exe程序爲你的客戶端生成代理類,然後把代理類從繼承於SoapHttpClientProtocol改爲繼承於WebServicesClientProtocol。這樣代理類就有了RequestSoapContext和ResponseSoapContext屬性,你可以使用它們訪問你發送或接收的WS-Security頭。在 C# 工程中,如果你已經使用了“Add Web Reference”選項,你可以點擊Solution Explorer中的“Show All Files”按鈕,點擊這個按鈕就可在Solution Explorer的Web References結點中顯示Reference.cs文件,讓你可以編輯這個文件。   爲了創建正確的UsernameToken和在消息級調用Web服務的代理方法,需要使用下面的代碼: private void Button1_Click(object sender, System.EventArgs e) {  localhost.SecurityService Wse wse =new

(txtUsername.Text,txtPassword.Text,PasswordOption.SendHashed);   wse .RequestSoapContext.Security.Tokens.Add (tkn);  try  {   DataSet ds= wse .CustOrderHist(txtCustID.Text);   DataGrid1.DataSource=ds;   DataGrid1.DataBind();  }  catch(Exception ex)  {   DataGrid1.Visible=false;   lblMessages.Text=ex.Message;  } }   我們要做的就是從客戶端的兩個文本輸入框txtUsername和txtPassword中取得輸入字串,然後使用PasswordOption.SendHashed把它們結合起來創建一個有效的UserNameToken。當調用Web服務時, WSE SOAP擴展驗證請求的一般格式,然後覈對密碼散列並從我們的PasswordProvider方法中取得的密碼。如果兩者匹配,我們就可調用Web服務方法,客戶端返回數據集,顯示在一個網格中。   我們現在已經創建了一個完整的使用 WSE 配合數據庫驗證SHA1摘要散列用戶名/密碼的Web服務,希望讀者們能通過本文瞭解到使用 WSE 保證Web服務安全的基本措施和方法,並能在實際工作中合理的去應用。   在文章的最後,我們給出修改Northwind數據庫Employees表的SQL腳本,給這個表添加了所需的username和password列

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章