
APISIX-MCP:利用 AI + MCP 擁抱 API 智能化管理
在這里插入圖片描述但筆者覺得那是一種反模式,而保留原有 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);
}
}
[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