WebService安全機制的思考與實踐
近來因業務需要,需要研究webservice,於是便有這篇文章:
SpringBoot整合Apache-CXF實踐
一、WebService是什麼?
WebService是一個平臺獨立的、低耦合的、自包含的、基於可程式設計的web的應用程式,可使用開放的XML(標準通用標記語言下的一個子集)標準來描述、釋出、發現、協調和配置這些應用程式,用於開發分散式的互動操作的應用程式。
簡單概括如下:
WebService是一種跨平臺,跨語言的規範,用於不同平臺,不同語言開發的應用之間的互動。
二、Webservice安全機制有哪些?
由於我之前從未實際接觸過WebService,對於它的安全機制不瞭解。於是通過搜尋,我得到了關於它的安全機制一些建議:
-
(1)對webservice釋出的方法,方法名稱和引數不要使用望文生義的描述;
-
(2)對webservice釋出的方法,在入參中增加一個或多個字串序列(這裡的字串可以要求必須滿足指定的格式,同時字串可以再通過客戶端傳引數的時候加密,服務端解密);
-
(3)對webservice釋出的方法,入參中加上使用者名稱和密碼,然後服務端通過資料庫校驗;
-
(4)對webservice釋出的方法,通過handler/chain方式來實現驗證(使用者名稱&密碼校驗/IP地址校驗等);
-
(5)對webservice釋出的方法,採用webservice的users.lst來進行驗證;
-
(6)對webservice釋出的服務,通過servlet的Filter來實現驗證;
-
(7)對webservice傳輸過程中的資料進行加密;
-
(8)自己寫校驗框架來實現webservice的安全;
-
(9)其它方式.
上述是搜尋方面出現畢竟頻繁的,也是webservice比較普遍的方式之一。
我思慮再三決定結合以往開發HTTP應用安全經驗和現有參考WebService安全機制結合起來。
於是便有了如下的安全機制方案:
- Token鑑權機制;
- 公私鑰簽名校驗;
- IP白名單校驗.
三、如何實現Token鑑權、公私鑰簽名校驗、IP白名單校驗等WebService安全方案呢?
本次程式碼已同步到我的Apache CXF程式碼例子裡了,Github地址為:
https://github.com/developers-youcong/blog-cxf
核心程式碼,關鍵在於攔截器
package com.blog.cxf.server.interceptor; import cn.hutool.core.util.StrUtil; import com.blog.cxf.server.security.SecretKey; import com.blog.cxf.server.utils.IpUtils; import lombok.extern.slf4j.Slf4j; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.headers.Header; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.message.Message; import org.apache.cxf.phase.AbstractPhaseInterceptor; import org.apache.cxf.phase.Phase; import org.apache.cxf.phase.PhaseInterceptorChain; import org.apache.cxf.transport.http.AbstractHTTPDestination; import org.springframework.stereotype.Component; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.servlet.http.HttpServletRequest; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; /** * @description: 認證鑑權攔截器 * @author: youcong * @time: 2020/10/31 17:07 */ @Slf4j @Component public class AuthInterceptor extends AbstractPhaseInterceptor<SoapMessage> { public AuthInterceptor() { super(Phase.PRE_INVOKE); } public void handleMessage(SoapMessage msg) throws Fault { Message ipVerify = PhaseInterceptorChain.getCurrentMessage(); HttpServletRequest request = (HttpServletRequest) ipVerify.get(AbstractHTTPDestination.HTTP_REQUEST); //處理IP handleIp(request); Header authHeader = null; //獲取驗證頭 List<Header> headers = msg.getHeaders(); if (headers.isEmpty()) { throw new Fault(new Exception("請求頭為空")); } for (Header h : headers) { log.info("h:" + h.getName().toString().contains("auth")); if (h.getName().toString().contains("auth")) { authHeader = h; break; } else { throw new Fault(new Exception("請求頭需包含auth")); } } Element auth = (Element) authHeader.getObject(); NodeList childNodes = auth.getChildNodes(); Set<String> reqHeader = new HashSet<String>(); for (int i = 0; i < childNodes.getLength(); i++) { //處理節點 handleNode(childNodes.item(i), reqHeader); } //處理請求Key handleSOAPReqHeader(reqHeader); } //處理IP private void handleIp(HttpServletRequest request) { String[] ip_arr = new String[]{"127.0.0.1", "192.168.52.50"}; for (String str : ip_arr) { System.out.println("ip:" + str); } Set<String> ipSet = new HashSet<String>(); for (String item : ip_arr) { ipSet.add(item); if (ipSet.contains(IpUtils.getIpAddr(request))) { log.info("合法IP:" + item); } else { throw new Fault(new Exception("非法IP")); } } } //處理節點 private void handleNode(Node items, Set<String> reqHeader) { Node item = items; //儲存請求頭Key if (item.getLocalName() != null) { String str = new String(item.getLocalName()); reqHeader.add(str); } //獲取請求頭token if (item.getNodeName().contains("token")) { String tokenValue = item.getTextContent(); if (!StrUtil.isEmpty(tokenValue)) { if ("soap".equals(tokenValue)) { log.info("token Value:" + tokenValue); } else { throw new Fault(new Exception("token錯誤")); } } else { throw new Fault(new Exception("token不能為空")); } } //獲取請求頭sign if (item.getNodeName().contains("sign")) { String signValue = item.getTextContent(); if (!StrUtil.isEmpty(signValue)) { //原資料 String originData = "test_webservice_api_2020"; try { //比對簽名 boolean verifySign = SecretKey.verifySign(originData, signValue); log.info("verifySign:" + verifySign); if (verifySign) { log.info("sign Value:" + signValue); } else { throw new Fault(new Exception("簽名錯誤")); } } catch (Exception e) { throw new Fault(new Exception("簽名錯誤")); } } else { throw new Fault(new Exception("簽名不能為空")); } } } //處理SOAP請求頭Key private void handleSOAPReqHeader(Set<String> reqHeader) { if (reqHeader.contains("token")) { log.info("包含token"); } else { throw new Fault(new Exception("請求頭auth需包含token")); } if (reqHeader.contains("sign")) { log.info("包含sign"); } else { throw new Fault(new Exception("請求頭auth需包含sign")); } } }
1.Token鑑權的目的是什麼?
每個使用者生成的token不一樣,獲取token的介面是需要對應的使用者名稱和密碼,通過使用者名稱和密碼產生token,token放在請求頭裡,後臺可根據token識別是哪個使用者請求哪個介面,後面日誌儲存會提到的。
2.Token的生成有哪些方案?
可以參考我寫的這篇文章:SpringCloud之Security
這篇文章我結合了JWT。
除此之外還可以結合某種規則(使用者名稱+密碼+特殊UUID+使用者註冊碼)生成加密的token。
3.簽名的目的是什麼?
為了資料安全和防止重複提交。
4.如何實現簽名?
簽名的規則有很多,可以增加某種證書公私鑰,也可以時間戳。
5.為什麼需要IP白名單校驗?
主要是為了安全,防止非法IP不停的請求,造成惡意攻擊(如DOS攻擊和DDOS攻擊等)。
6.IP白名單校驗有哪些方案?
可以將IP白名單放在對應的資料表中,也可以將其放到配置檔案裡,還可以將其存一個數組中(就像我在上述程式碼所寫的那樣)。
7.開始測試
(1)非法IP請求(不在陣列內的IP)
(2)攜帶錯誤的Token請求
(3)攜帶錯誤的簽名請求
(4)正確請求(token正確、簽名正確、IP合法)
8.證書生成方案(公私鑰)
這一塊我主要參考了這篇文章,這篇文章很完整,大家可以參考一下:
Java 證書(keytool例項)程式碼實現加解密、加簽、驗籤
生成證書核心兩條命令,如下(注意,其中的密碼之類的,改成自己的):
## 生成私鑰 keytool -genkey -alias yunbo2 -keypass 123456 -keyalg RSA -keysize 1024 -validity 3650 -keystore merKey.jks -storepass abc@2018 -dname "CN=localhost,OU=localhost, O=localhost, L=深圳, ST=廣東, C=CN" ## 生成公鑰 keytool -export -alias yunbo2 -keystore merKey.jks -file yunbo2.cer
9.資料加密
資料加密主要體現在對請求體內的資料進行base64加密或者是其他的加密方式。
10.補充說明
之前搜尋了不少文章提到過,請求頭或者請求體傳輸使用者名稱和密碼,我個人覺得使用者名稱和密碼傳輸太過頻繁並不安全,因此我選擇了token,選擇了多一步(通過使用者名稱和密碼拿到token,再通過token請求對其它業務webservice等)。
四、總結
技術往往有很多相似之處,可以複用和借鑑。之前在研究Apache CXF安全機制的時候,發現並沒有那麼多的資料可供參考,於是我換了一個思路,Apache CXF框架本質上就是對WebService簡化,方便開發人員使用而不用配置一堆東西。我把核心聚焦在webservice安全,然後在發散,就有了這篇文章。
簡單的概括一點:
遇到難題不要鑽牛角尖,可以嘗試換一個思路(發散自己的思維)來解決這個難題。
&n