WebService安全機制的思考與實踐

近來因業務須要,須要研究webservice,因而便有這篇文章:
SpringBoot整合Apache-CXF實踐

html

1、WebService是什麼?

WebService是一個平臺獨立的、低耦合的、自包含的、基於可編程的web的應用程序,可以使用開放的XML(標準通用標記語言下的一個子集)標準來描述、發佈、發現、協調和配置這些應用程序,用於開發分佈式的交互操做的應用程序。java

簡單歸納以下:
WebService是一種跨平臺,跨語言的規範,用於不一樣平臺,不一樣語言開發的應用之間的交互git

2、Webservice安全機制有哪些?

因爲我以前從未實際接觸過WebService,對於它的安全機制不瞭解。因而經過搜索,我獲得了關於它的安全機制一些建議:github

  • (1)對webservice發佈的方法,方法名稱和參數不要使用望文生義的描述;web

  • (2)對webservice發佈的方法,在入參中增長一個或多個字符串序列(這裏的字符串能夠要求必須知足指定的格式,同時字符串能夠再經過客戶端傳參數的時候加密,服務端解密);spring

  • (3)對webservice發佈的方法,入參中加上用戶名和密碼,而後服務端經過數據庫校驗;數據庫

  • (4)對webservice發佈的方法,經過handler/chain方式來實現驗證(用戶名&密碼校驗/IP地址校驗等);apache

  • (5)對webservice發佈的方法,採用webservice的users.lst來進行驗證;編程

  • (6)對webservice發佈的服務,經過servlet的Filter來實現驗證;api

  • (7)對webservice傳輸過程當中的數據進行加密;

  • (8)本身寫校驗框架來實現webservice的安全;

  • (9)其它方式.

上述是搜索方面出現畢竟頻繁的,也是webservice比較廣泛的方式之一。

我思慮再三決定結合以往開發HTTP應用安全經驗和現有參考WebService安全機制結合起來。

因而便有了以下的安全機制方案:

  • Token鑑權機制;
  • 公私鑰簽名校驗;
  • IP白名單校驗.

3、如何實現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等)。

4、總結

技術每每有不少類似之處,能夠複用和借鑑。以前在研究Apache CXF安全機制的時候,發現並無那麼多的資料可供參考,因而我換了一個思路,Apache CXF框架本質上就是對WebService簡化,方便開發人員使用而不用配置一堆東西。我把核心聚焦在webservice安全,而後在發散,就有了這篇文章。
簡單的歸納一點:
遇到難題不要鑽牛角尖,能夠嘗試換一個思路(發散本身的思惟)來解決這個難題。

相關文章
相關標籤/搜索