基於非對稱加密和驗簽實現介面的安全開放(openapi的實現)

来源:https://www.cnblogs.com/hdwang/archive/2022/05/27/16319481.html
-Advertisement-
Play Games

背景 對外服務的介面為了安全起見,往往需要進行相應的安全處理:數據加密傳輸和身份認證。數據加密傳輸有對稱加密和非對稱加密兩種,為了更加安全起見採用非對稱加密比較好些,身份認證則採用數字簽名可以實現。 程式流程 核心代碼 客戶端 package openapi.client.sdk; import c ...


背景

對外服務的介面為了安全起見,往往需要進行相應的安全處理:數據加密傳輸和身份認證。數據加密傳輸有對稱加密和非對稱加密兩種,為了更加安全起見採用非對稱加密比較好些,身份認證則採用數字簽名可以實現。

 

程式流程

 

 

 

核心代碼

客戶端

package openapi.client.sdk;

import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.*;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import openapi.sdk.common.constant.Constant;
import openapi.sdk.common.model.AsymmetricCryEnum;
import openapi.sdk.common.model.BusinessException;
import openapi.sdk.common.model.InParams;
import openapi.sdk.common.model.OutParams;
import openapi.sdk.common.util.Base64Util;

import java.nio.charset.StandardCharsets;

import static openapi.sdk.common.util.Base64Util.bytesToBase64;

/**
 * 對外開放api客戶端
 *
 * @author wanghuidong
 */
@Slf4j
public class OpenApiClient {

    private String baseUrl;
    private String selfPrivateKey;
    private String remotePublicKey;
    private AsymmetricCryEnum asymmetricCryEnum;
    private boolean retDecrypt;

    /**
     * 客戶端系統的私鑰
     *
     * @param baseUrl           openapi基礎路徑
     * @param selfPrivateKey    本系統私鑰
     * @param remotePublicKey   遠程系統的公鑰
     * @param asymmetricCryEnum 非對稱加密演算法
     * @param retDecrypt        返回值是否需要解密
     */
    public OpenApiClient(String baseUrl, String selfPrivateKey, String remotePublicKey, AsymmetricCryEnum asymmetricCryEnum, boolean retDecrypt) {
        this.baseUrl = baseUrl;
        this.selfPrivateKey = selfPrivateKey;
        this.remotePublicKey = remotePublicKey;
        this.asymmetricCryEnum = asymmetricCryEnum;
        this.retDecrypt = retDecrypt;
    }

    /**
     * 調用openapi
     *
     * @param inParams 入參
     * @return 返回值
     */
    public OutParams callOpenApi(InParams inParams) {
        //加密&加簽
        encryptAndSign(inParams);

        //調用openapi 並 處理返回值
        OutParams outParams = doCall(inParams);
        return outParams;
    }

    /**
     * 加密&加簽
     *
     * @param inParams 入參
     */
    private void encryptAndSign(InParams inParams) {
        String body = inParams.getBody();
        if (StrUtil.isNotBlank(body)) {
            //加密
            if (asymmetricCryEnum == AsymmetricCryEnum.RSA) {
                RSA rsa = new RSA(null, remotePublicKey);
                byte[] encrypt = rsa.encrypt(StrUtil.bytes(body, CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);
                body = bytesToBase64(encrypt);
            } else if (asymmetricCryEnum == AsymmetricCryEnum.SM2) {
                SM2 sm2 = SmUtil.sm2(null, remotePublicKey);
                body = sm2.encryptBcd(body, KeyType.PublicKey);
            } else {
                throw new BusinessException("不支持的非對稱加密演算法");
            }
            inParams.setBody(body);

            //加簽
            String signedStr = null;
            if (asymmetricCryEnum == AsymmetricCryEnum.RSA) {
                Sign sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, selfPrivateKey, null);
                //簽名
                byte[] data = body.getBytes(StandardCharsets.UTF_8);
                byte[] signed = sign.sign(data);
                signedStr = bytesToBase64(signed);
            } else if (asymmetricCryEnum == AsymmetricCryEnum.SM2) {
                SM2 sm2 = SmUtil.sm2(selfPrivateKey, null);
                signedStr = sm2.signHex(HexUtil.encodeHexStr(body));
            } else {
                throw new BusinessException("不支持的非對稱加密演算法");
            }
            inParams.setSign(signedStr);
        }
    }

    /**
     * 調用遠程openapi介面
     *
     * @param inParams 入參
     * @return 結果
     */
    private OutParams doCall(InParams inParams) {
        String url = URLUtil.completeUrl(baseUrl, Constant.OPENAPI_PATH);
        String body = JSONUtil.toJsonStr(inParams);
        log.debug("調用openapi入參:" + inParams);
        String ret = HttpUtil.post(url, body);
        log.debug("調用openapi返回值:" + ret);
        if (StrUtil.isBlank(ret)) {
            throw new BusinessException("返回值為空");
        }
        OutParams outParams = JSONUtil.toBean(ret, OutParams.class);
        if (OutParams.isSuccess(outParams)) {
            log.debug("調用openapi成功");
            //判斷是否需要解密數據
            if (retDecrypt) {
                decryptData(outParams);
            }
        } else {
            throw new BusinessException("調用openapi異常:" + outParams.getMessage());
        }
        return outParams;
    }

    /**
     * 解密數據
     *
     * @param outParams 返回值
     */
    private void decryptData(OutParams outParams) {
        //解密
        String decryptedData = null;
        try {
            if (asymmetricCryEnum == AsymmetricCryEnum.RSA) {
                RSA rsa = new RSA(selfPrivateKey, null);
                byte[] dataBytes = Base64Util.base64ToBytes(outParams.getData());
                byte[] decrypt = rsa.decrypt(dataBytes, KeyType.PrivateKey);
                decryptedData = new String(decrypt, StandardCharsets.UTF_8);
            } else if (asymmetricCryEnum == AsymmetricCryEnum.SM2) {
                SM2 sm2 = SmUtil.sm2(selfPrivateKey, null);
                decryptedData = StrUtil.utf8Str(sm2.decryptFromBcd(outParams.getData(), KeyType.PrivateKey));
            } else {
                throw new BusinessException("不支持的非對稱加密演算法");
            }
            outParams.setData(decryptedData);
        } catch (Exception ex) {
            log.error("解密失敗", ex);
            throw new BusinessException("解密失敗");
        }
    }


}

服務端

package openapi.server.sdk;

import cn.hutool.core.util.*;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.*;
import lombok.extern.slf4j.Slf4j;
import openapi.server.sdk.model.ApiHandler;
import openapi.server.sdk.model.OpenApi;
import openapi.server.sdk.model.OpenApiMethod;
import openapi.sdk.common.constant.Constant;
import openapi.sdk.common.model.AsymmetricCryEnum;
import openapi.sdk.common.model.BusinessException;
import openapi.sdk.common.model.InParams;
import openapi.sdk.common.model.OutParams;
import openapi.sdk.common.util.Base64Util;
import openapi.sdk.common.util.StrObjectConvert;
import openapi.server.sdk.config.OpenApiConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * 對外開放api網關入口
 * <p>
 * 功能
 * 1.負責對外開放介面(基於HTTP對外提供服務)
 * 2.實現介面的參數與返回值的加解密(使用RSA或國密SM2實現非對稱加解密)
 * 3.實現介面的驗簽(服務端會校驗客戶端的簽名,確保調用者身份以及數據不被篡改)
 * <p>
 *
 * @author wanghuidong
 */
@Slf4j
@RestController
public class OpenApiGateway {

    /**
     * 定義api處理器映射
     * key: api_method
     * value: ApiHandler
     */
    private Map<String, ApiHandler> handlerMap = new HashMap<>();

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private OpenApiConfig config;

    /**
     * 初始化
     */
    @PostConstruct
    public void init() {
        //初始化所有的openapi處理器
        Map<String, Object> beanMap = applicationContext.getBeansWithAnnotation(OpenApi.class);
        for (Map.Entry<String, Object> entry : beanMap.entrySet()) {
            Object bean = entry.getValue();
            Class c = bean.getClass();
            //獲取開放api名稱
            OpenApi openApi = (OpenApi) c.getAnnotation(OpenApi.class);
            String openApiName = openApi.value();
            //遍歷方法
            Method[] methods = c.getDeclaredMethods();
            if (ArrayUtil.isNotEmpty(methods)) {
                for (Method method : methods) {
                    if (method.isAnnotationPresent(OpenApiMethod.class)) {
                        //獲取開放api方法名稱
                        OpenApiMethod openApiMethod = method.getAnnotation(OpenApiMethod.class);
                        String openApiMethodName = openApiMethod.value();

                        //獲取方法參數
                        Class[] classes = method.getParameterTypes();

                        //保存處理器到Map中
                        String handlerKey = getHandlerKey(openApiName, openApiMethodName);
                        ApiHandler apiHandler = new ApiHandler();
                        apiHandler.setBean(bean);
                        apiHandler.setMethod(method);
                        apiHandler.setParamClasses(classes);
                        handlerMap.put(handlerKey, apiHandler);
                    }
                }
            }
        }
    }


    /**
     * 調用具體的方法
     *
     * @param inParams 入參
     * @return 出參
     */
    @PostMapping(Constant.OPENAPI_PATH)
    public OutParams callMethod(@RequestBody InParams inParams) {
        OutParams outParams = null;
        try {
            log.debug("接收到請求:" + inParams);

            //獲取openapi處理器
            ApiHandler apiHandler = getApiHandler(inParams);

            //獲取方法參數
            Object param = getParam(inParams, apiHandler);

            //調用目標方法
            return outParams = doCall(apiHandler, param, inParams);
        } catch (BusinessException be) {
            log.error(be.getMessage());
            return outParams = OutParams.error(be.getMessage());
        } catch (Exception ex) {
            log.error("系統異常:", ex);
            return outParams = OutParams.error("系統異常");
        } finally {
            outParams.setUuid(inParams.getUuid());
            log.debug("調用完畢:" + outParams);
        }
    }

    /**
     * 獲取openapi處理器
     *
     * @param inParams 入參
     * @return openapi處理器
     */
    private ApiHandler getApiHandler(InParams inParams) {
        String handlerKey = getHandlerKey(inParams.getApi(), inParams.getMethod());
        ApiHandler apiHandler = handlerMap.get(handlerKey);
        if (apiHandler == null) {
            throw new BusinessException("找不到指定的opeapi處理器");
        }
        return apiHandler;
    }

    /**
     * 獲取方法參數
     *
     * @param inParams   入參
     * @param apiHandler openapi處理器
     * @return 方法參數
     */
    private Object getParam(InParams inParams, ApiHandler apiHandler) {
        Object param = null;
        if (StrUtil.isNotBlank(inParams.getBody())) {
            //驗簽
            boolean verify = false;
            String callerPublicKey = config.getCallerPublicKey(inParams.getCallerId());
            if (config.getAsymmetricCry() == AsymmetricCryEnum.RSA) {
                Sign sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, null, callerPublicKey);
                verify = sign.verify(inParams.getBody().getBytes(StandardCharsets.UTF_8), Base64Util.base64ToBytes(inParams.getSign()));
            } else if (config.getAsymmetricCry() == AsymmetricCryEnum.SM2) {
                SM2 sm2 = SmUtil.sm2(null, callerPublicKey);
                verify = sm2.verifyHex(HexUtil.encodeHexStr(inParams.getBody()), inParams.getSign());
            } else {
                throw new BusinessException("不支持的非對稱加密演算法");
            }
            if (!verify) {
                throw new BusinessException("驗簽失敗");
            }

            //解密
            String decryptedBody = null;
            try {
                String selfPrivateKey = config.getSelfPrivateKey();
                if (config.getAsymmetricCry() == AsymmetricCryEnum.RSA) {
                    RSA rsa = new RSA(selfPrivateKey, null);
                    byte[] bodyBytes = Base64Util.base64ToBytes(inParams.getBody());
                    byte[] decrypt = rsa.decrypt(bodyBytes, KeyType.PrivateKey);
                    decryptedBody = new String(decrypt, StandardCharsets.UTF_8);
                } else if (config.getAsymmetricCry() == AsymmetricCryEnum.SM2) {
                    SM2 sm2 = SmUtil.sm2(selfPrivateKey, null);
                    decryptedBody = StrUtil.utf8Str(sm2.decryptFromBcd(inParams.getBody(), KeyType.PrivateKey));
                } else {
                    throw new BusinessException("不支持的非對稱加密演算法");
                }
            } catch (Exception ex) {
                log.error("解密失敗", ex);
                throw new BusinessException("解密失敗");
            }

            try {
                //當前僅支持一個參數的方法
                Class paramClass = apiHandler.getParamClasses()[0];
                param = StrObjectConvert.strToObj(decryptedBody, paramClass);
            } catch (Exception ex) {
                log.error("入參轉換異常", ex);
                throw new BusinessException("入參轉換異常");
            }
        }
        return param;
    }

    /**
     * 調用目標方法
     *
     * @param apiHandler openapi處理器
     * @param param      方法參數
     * @param inParams   openapi入參
     * @return 返回結果
     */
    private OutParams doCall(ApiHandler apiHandler, Object param, InParams inParams) {
        try {
            Object ret = apiHandler.getMethod().invoke(apiHandler.getBean(), param);
            String retStr = StrObjectConvert.objToStr(ret, ret.getClass());
            //返回值需要加密
            if (config.retEncrypt()) {
                retStr = encryptRet(inParams, retStr);
            }
            return OutParams.success(retStr);
        } catch (Exception ex) {
            log.error("調用opeapi處理器異常", ex);
            throw new BusinessException("調用opeapi處理器異常");
        }
    }

    /**
     * 加密返回值
     *
     * @param inParams openapi入參
     * @param retStr   返回值
     * @return 加密後的返回值
     */
    private String encryptRet(InParams inParams, String retStr) {
        try {
            String callerPublicKey = config.getCallerPublicKey(inParams.getCallerId());
            if (config.getAsymmetricCry() == AsymmetricCryEnum.RSA) {
                RSA rsa = new RSA(null, callerPublicKey);
                byte[] encrypt = rsa.encrypt(StrUtil.bytes(retStr, CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);
                retStr = Base64Util.bytesToBase64(encrypt);
            } else if (config.getAsymmetricCry() == AsymmetricCryEnum.SM2) {
                SM2 sm2 = SmUtil.sm2(null, callerPublicKey);
                retStr = sm2.encryptBcd(retStr, KeyType.PublicKey);
            } else {
                throw new BusinessException("不支持的非對稱加密演算法");
            }
        } catch (Exception ex) {
            throw new BusinessException("返回值加密異常", ex);
        }
        return retStr;
    }


    /**
     * 獲取api處理器Map的key
     *
     * @param openApiName       開放api名稱
     * @param openApiMethodName 開放api方法名稱
     * @return 處理器Map的key
     */
    private String getHandlerKey(String openApiName, String openApiMethodName) {
        return openApiName + "_" + openApiMethodName;
    }


}

 

項目完整源碼

https://github.com/hdwang123/openapi

目前該項目已經發佈jar包到maven中央倉庫,大伙可以直接引入項目中進行快速開發。具體的maven依賴配置如下:

        <!-- openapi服務端sdk -->
        <dependency>
            <groupId>io.github.hdwang123</groupId>
            <artifactId>openapi-server-sdk</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!-- openapi客戶端sdk -->
        <dependency>
            <groupId>io.github.hdwang123</groupId>
            <artifactId>openapi-client-sdk</artifactId>
            <version>1.0.0</version>
        </dependency>

具體的使用見github。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近在數據處理中用到了窗函數, 把使用方法記錄一下, 暫時只有分組排序和滑動時間視窗的例子, 以後再逐步添加. 在SQL查詢時, 會遇到有兩類需要分組統計的場景, 在之前的SQL語法中是不方便實現的. 使用窗函數直接SQL中使用窗函數就能解決這些問題, 否則需要使用臨時表, 函數或存儲過程進行處理.... ...
  • 本文介紹如何使用 CREATE DATABASE 語句創建資料庫、 CREATE TABLE 語句創建表、ALTER TABLE 語句更新表、DROP TABLE 語句刪除表。 一、表的創建 本節要點 表通過 CREATE TABLE 語句創建而成。 表和列的命名要使用有意義的文字。 指定列的數據類 ...
  • 5月24日,由華為開發者聯盟主辦的HUAWEI Developer Day(華為開發者日,簡稱HDD)線上沙龍·創新開發專場在華為開發者學堂及各大直播平臺與廣大開發者見面。直播內容主要聚焦HarmonyOS和HMS生態應用開發,帶來關於HarmonyOS服務卡片、HMS Core開放能力、應用高效開 ...
  • 二維碼和條形碼從發明到發展已經過去了幾十年,因其能快捷方便讀取信息的特點,在數字經濟時代被廣泛應用。掃描二維碼可以識別健康狀況,識別身份信息、訪問網站鏈接、完成金融支付等等,已經成為生活中不可或缺的實用技術,所以很多App都搭載了“掃一掃”功能。 然而,在日常掃碼過程中,我們也經常會遇到掃碼環境暗、 ...
  • 1.字元集/字元編碼是什麼? 字元集或者說字元編碼就是給字元定義了數值編號以及數值編號存儲格式。 嚴格來說字元集和字元編碼是兩個概念: charset 是 character set 的簡寫,即字元集。 encoding 是 charset encoding 的簡寫,即字元集編碼,簡稱編碼。 字元集 ...
  • vant 的表單校驗 個人理解: 將rules當成一個對象去理解,傳參時可以是整個對象或者對象的某一屬性 常用兩種校驗方式 1, 正則表達式 1.1自定義校驗規則(校驗規格也可傳入多條): 表單: :rules="[{ pattern:ageRules, message: '請填寫密碼' }]" d ...
  • 深居內陸的人們,大概每個人都有過大海之夢吧。夏日傍晚在沙灘漫步奔跑;或是在海上衝浪游泳;或是在海島游玩探險;亦或靜待日出日落……本文使用 React + Three.js 技術棧,實現 3D 海洋和島嶼,主要包含知識點包括:Tone Mapping、Water 類、Sky 類、Shader 著色、S... ...
  • 大家好,我是三友,這篇文章想來跟大家來探討一下,在Java中已經提供了併發安全的集合,為什麼有的場景還需要使用讀寫鎖,直接用併發安全的集合難道不行麽? 在java中,併發安全的集合有很多,這裡我就選用常見的CopyOnWriteArrayList為例,來說明一下讀寫鎖的價值到底提現在哪。 CopyO ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...