在這里插入圖片描述但筆者覺得那是一種反模式,而保留原有 JSON 結構更好,如下提交的 JSON。

ounter(lineounter(lineounter(lineounter(line
{
"errCode": "0",
"data": "BQduoGH4PI+6jxgu+6S2FWu5c/vHd+041ITnCH9JulUKpPX8BvRTvBNYfP7……"
}

另外也符合既有的統一返回結果,即把

data

數據加密,其他

code

msg

等的正常顯示。

系統要求

只支持 Spring + Jackson 的方案。

加密算法

加密算法需要調用方(如瀏覽器)與 API 接口協商好。一般采用 RSA 加密算法。雖然 RSA 沒 AES 速度高,但勝在是非對稱加密,AES 這種對稱加密機制在這場合就不適用了(因為瀏覽器是不能放置任何密鑰的,——除非放置非對稱的公鑰)。當然,如果你設計的 API 接口給其他第三方調用而不是瀏覽器,可以保證密鑰安全的話,那么使用 AES 也可以,包括其他摘要算法同理亦可,大家商定好算法(md5/sha1/sha256……)和鹽值(Slat)即可。該組件當前僅支持 RSA(1024bit key)。下面更多的算法在路上。

使用方式

初始化在 YAML 配置中加入:

api:
EncryptedBody:
enable: true
publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmkKluNutOWGmAK2U……
privateKey: MIICdgIBADANBgkqhkiG9w0BAQ……

主要是 RSA 的公鑰/私鑰。然后在 Spring 配置類

WebMvcConfigurer

中加入:

@Value("${api.EncryptedBody.publicKey}")
private String apiPublicKey;
@Value("${api.EncryptedBody.privateKey}")
private String apiPrivateKey;
@Value("${api.EncryptedBody.enable}")
private boolean apiEncryptedBodyEnable;
@Override
public void configureMessageConverters(List<HttpMessageConverter> converters) {
EncryptedBodyConverter converter = new EncryptedBodyConverter(apiPublicKey, apiPrivateKey);
converter.setEnabled(apiEncryptedBodyEnable);
converters.add(0, converter);
}

配置要加密的數據使用方式很簡單,其實就是添加一個 Java 注解

@EncryptedData

到你的 Java Bean 上即可。不過我們還是按照正兒八經的循序漸進的方式去看看。首先是解密請求的數據,我們觀察這個 Spring MVC 接口聲明,與一般的 JSON 提交數據方式無異,添加了注解

@RequestBody

,其他無須修改:

@PostMapping("/submit")
boolean jsonSubmit(@RequestBody User user);

重點是 User 這個 DTO,為了標明是加密數據,需要在這個 Bean 上聲明我們自定義的注解

@EncryptedData

package com.ajaxjs.api.encryptedbody;
@EncryptedData
public class User {
private String name;
private int age;
// Getters and Setters
}

同時我們提交的對象不再是 User 的 JSON,而是

DecodeDTO

(雖然最終轉換為

User

,成功解密的話),即:

package com.ajaxjs.api.encryptedbody;
import lombok.Data;
@Data
public class DecodeDTO {
/**
* Encrypted data
*/
private String data;
}

當然你可以修改這個 DTO 為你符合的結構。提交的樣子就是像:

{
"data": "BQduoGH4PI+6jxgu+6S2FWu5c/vHd+041ITnCH9JulUKpPX8BvRTvBNYfP7……"
}

這個加密過的密文怎么來的?當然是你客戶端加密后的結果。或者從下面小節說的方式,返回一段密文。

返回加密的數據

下面 Controller 方法返回一個 User 對象,沒有任何修改。

@GetMapping("/user")
User User();
……
@Override
public User User() {
User user = new User();
user.setAge(1);
user.setName("tom");
return user;
}

我們同樣需要加一個注解

@EncryptedData

即可對其加密。當前版本中暫不支持字段級別的加密,只支持整個對象加密。返回結果如下:

{
"status": 1,
"errorCode": null,
"message": "操作成功",
"data": "ReSSPC34JE+O/SmLCxE5zVJb6D2tzp1f5pfQyKdjvOWkQQ+qDjcjw/2m/KPA+2+uc9kseqFryXNPIZCEfsaOCJAqzMtrXyZ0JPB1skeJxKOngS5USijsY0UZqN9hLS3O/7CBLlSGkEuyXZV//WcWDG9BpQ4TAKrlRfwM4bnCo+E="
}

添加依賴

哦~對了,別忘了添加依賴,——沒單獨搞 jar 包,直接 copy 代碼吧,才三個類:源碼 [1]。其中

ResponseResultWrapper

就是統一返回結果的類,你可以改為你項目的,——其他的沒啥依賴了,——還有就是 RSA 依賴我的工具包:


com.ajaxjs
ajaxjs-util
1.1.8

很小巧的,才60kb 的 jar 包——請放心食用~

實現方式

這里說說實現原理,以及一些 API 設計風格的思考。我們這種的用法,相當于接收了 A 對象(加密的,

DecodeDTO

),轉換為 B 對象(解密的,供控制器使用)。最簡單的方式就是這樣的:

@PostMapping("/submit")
boolean jsonSubmit(@RequestBody DecodeDTO dto) {
User user = 轉換函數(dto.getData());
}

但是這種方法,方法數量一多則遍地

DecodeDTO

,API 文檔也沒法寫了(破壞了代碼清晰度,不能反映原來代碼的意圖)。為此我們應該盡量采用“非入侵”的方法,所謂非入侵,就是不修改原有的代碼,只做額外的“裝飾”。這種手段有很多,典型如 AOP,其他同類的開源庫sa-encrypt-body-spring-boot[2]、encrypt-body-spring-boot-starter[3]也是不約而同地使用 AOP。然而筆者個人來說不太喜歡 AOP,可能也是不夠熟悉吧——反正能不用則不用。如果不用 AOP 那應該如何做呢?筆者思考了幾種方式例如 Filter、攔截器等,但最終把這個問題定位于 JSON 序列化/反序列化層面上,在執行這一步驟之前就可以做加密/解密操作了。開始以為可以修改 Jackson 全局序列化方式,但礙于全局的話感覺不太合理,更合適的是在介乎于 Spring 與 Jackson 結合的地方做修改。于是有了在的

MappingJackson2HttpMessageConverter

基礎上擴展的

EncryptedBodyConverter

,重寫了

read

方法,在反序列化之前先做解密操作,

writeInternal

方法亦然。核心方法就一個類,不足一百行代碼:

import com.ajaxjs.springboot.ResponseResultWrapper;
import com.ajaxjs.util.EncodeTools;
import com.ajaxjs.util.cryptography.RsaCrypto;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.io.IOException;
import java.lang.reflect.Type;
public class EncryptedBodyConverter extends MappingJackson2HttpMessageConverter {
public EncryptedBodyConverter(String publicKey, String privateKey) {
super();
this.publicKey = publicKey;
this.privateKey = privateKey;
}
private final String publicKey;
private final String privateKey;
/**
* 使用私鑰解密字符串
*
* @param encryptBody 經過 Base64 編碼的加密字符串
* @param privateKey 私鑰字符串,用于解密
* @return 解密后的字符串
*/
static String decrypt(String encryptBody, String privateKey) {
byte[] data = EncodeTools.base64Decode(encryptBody);
return new String(RsaCrypto.decryptByPrivateKey(data, privateKey));
}
/**
* 使用公鑰加密字符串
* 

* 該方法采用RSA加密算法,使用給定的公鑰對一段字符串進行加密 * 加密后的字節數組被轉換為 Base64 編碼的字符串,以便于傳輸和存儲 * * @param body 需要加密的原始字符串 * @param publicKey 用于加密的公鑰字符串 * @return 加密后的 Base64 編碼字符串 */ static String encrypt(String body, String publicKey) { byte[] encWord = RsaCrypto.encryptByPublicKey(body.getBytes(), publicKey); return EncodeTools.base64EncodeToString(encWord); } /** * 重寫 read 方法以支持加密數據的讀取 * * @param type 數據類型,用于確定返回對象的類型 * @param contextClass 上下文類,未在本方法中使用 * @param inputMessage 包含加密數據的 HTTP 輸入消息 * @return 根據類型參數反序列化后的對象實例 * @throws IOException 如果讀取或解析過程中發生 I/O 錯誤 * @throws HttpMessageNotReadableException 如果消息無法解析為對象實例 */ @Override public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { Class clz = (Class) type; if (clz.getAnnotation(EncryptedData.class) != null) { ObjectMapper objectMapper = getObjectMapper(); DecodeDTO decodeDTO = objectMapper.readValue(inputMessage.getBody(), DecodeDTO.class); String encryptBody = decodeDTO.getData(); String decodeJson = decrypt(encryptBody, privateKey); return objectMapper.readValue(decodeJson, clz); } return super.read(type, contextClass, inputMessage); } @Override protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { Class clz = (Class) type; if (object instanceof ResponseResultWrapper && clz.getAnnotation(EncryptedData.class) != null) { ResponseResultWrapper response = (ResponseResultWrapper) object; Object data = response.getData(); String json = getObjectMapper().writeValueAsString(data); String encryptBody = encrypt(json, publicKey); response.setData(encryptBody); } super.writeInternal(object, type, outputMessage); } }

TODO

本文內鏈接

[1] 源碼: https://gitcode.com/zhangxin09/aj-framework/tree/master/aj-framework/src/main/java/com/ajaxjs/api/encryptedbody

[2] sa-encrypt-body-spring-boot: https://github.com/ishuibo/rsa-encrypt-body-spring-boot

[3] encrypt-body-spring-boot-starter: https://github.com/Licoy/encrypt-body-spring-boot-starter

原文轉載自:https://mp.weixin.qq.com/s/CrPN_w6K70vpzO3hWVzIwQ

上一篇:

探討 Go 中內存對齊的工作原理

下一篇:

使用Google Gemini API密鑰創建AI驅動的Chrome擴展程序
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費