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