目录
- 说明
- 实现效果
- 实现步骤
- 扩展
-
- Response响应格式实体
- [ResponseCode 响应状态码](#ResponseCode 响应状态码)
- RSA工具类
- [RequestBodyAdvice 介绍](#RequestBodyAdvice 介绍)
- [ResponseBodyAdvice 介绍](#ResponseBodyAdvice 介绍)
说明
由于项目中需要进行加密传输数据提高项目安全性,前端调用接口时,请求体中的数据全部加密成密文传输到后端接口,后端接口接收之后解密回明文。本例中使用的RSA非对称加解密方式处理。
如果后端接口参数使用字符串String 接收密文参数,则需要在每个接口方法中进行解密操做,代码会产生冗余。
如下伪代码:
java
@Slf4j
@RestController
@RequestMapping(value = "user")
public class TUserController {
/**
* 说明: 添加
* @author zhangxiaosan
* @create 2024/12/3
* @param
* @return
*/
@PostMapping(value = "add")
public String add(@RequestBody String user){
// user 参数为前端传递过来的 密文
// 使用RSA 解密方法,传入密文和密钥解密得到明文
String data = RSA.decryption(user,key); // 此部分为冗余代码。
// todo 业务操做逻辑
return user.toString();
}
}
现在,有个需求是,接口方法中正常使用实体类来作为参数类型接收。前端依旧是传递加密后的密文。后端接收到密文之后,解密得到明文后,将明文数据转成接口参数所需要的数据类型。
业务处理完成之后,将返回的数据再进行加密传给前端。前端解密后才能得到明文展示 。
实现效果
逻辑图
实现步骤
创建公共处理的请求和响应的类
此类主要是公共处理api接收前端请求的密文参数,解密。处理方法返回数据的加密。
java
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import www.three.common.response.Response;
import www.three.common.security.rsa.RSAUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Objects;
/**
* 说明:
* 对请求体和响应体进行加密解密
* 此类用于全局处理请求和响应的功能类
* RequestBodyAdvice 接口
* RequestBodyAdvice 接口用于在请求的请求体被反序列化(即转化为 Java 对象)之前对其进行处理。你可以使用它来解密请求体中的数据。
* <p>
* ResponseBodyAdvice<Response> 接口
* ResponseBodyAdvice 接口用于在响应体被序列化(即将 Java 对象转化为响应内容)之前对其进行处理。你可以使用它来加密响应体中的数据。Response 为自定义的响应格式实体类。
*
* @author 张小三
* @create 2024-12-04 10:00
* @verson 1.0.0
*/
@Slf4j
@ControllerAdvice
public class EncryptRequestResponseBodyAdvice implements ResponseBodyAdvice<Response>, RequestBodyAdvice {
// 前端 RSA算法的私钥,用于解密前端传递过来的密文。前端需要于此私钥对应的公钥进行加密数据。
private static final String privateKey = "";// 此处填写为前端的RSA私钥
// 后端Api RSA 算法的公钥,用于加密响应体的数据。前端需要使用此公钥对应的私钥才能进行数据解密。
private static final String publicKey = ""; // 此处填写为后端api的RSA公钥
/**
* 此方法判断当前处理器是否支持给定的方法参数、目标类型和HTTP消息转换器类型
* @param methodParameter 方法参数
* @param targetType 目标类型
* @param converterType HTTP消息转换器类型
* @return 总是返回false,表示不支持
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;//返回 true 表示我们想要处理所有请求的请求体
}
/**
* 在读取请求体之前调用此方法进行预处理
* @param inputMessage 输入消息
* @param parameter 方法参数
* @param targetType 目标类型
* @param converterType HTTP消息转换器类型
* @return 返回null,表示不需要进行特殊处理
* @throws IOException 如果处理过程中发生I/O错误
*/
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
// 读取响应体的流
InputStream inputStream = inputMessage.getBody();
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
// 转成字符串
String data = JSON.parseObject(bytes).getString("data");
log.info("RequestBody 密文:" + data);
// 使用RSA工具方法解密
data = RSAUtil.decryption(data,privateKey);
log.info("RequestBody 明文:" + data);
return new ByteArrayInputStream(data.getBytes());
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}
/**
* 在读取请求体之后调用此方法进行后处理
*
* @param body 请求体内容
* @param inputMessage 输入消息
* @param parameter 方法参数
* @param targetType 目标类型
* @param converterType HTTP消息转换器类型
* @return 返回null,表示不需要进行特殊处理
*/
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
/**
* 当请求体为空时调用此方法进行处理
*
* @param body 请求体内容(此时为空)
* @param inputMessage 输入消息
* @param parameter 方法参数
* @param targetType 目标类型
* @param converterType HTTP消息转换器类型
* @return 返回null,表示不需要进行特殊处理
*/
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
/**
* 此方法判断当前处理器是否支持给定的返回类型和HTTP消息转换器类型
* @param returnType 返回类型
* @param converterType HTTP消息转换器类型
* @return 总是返回false,表示不支持
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
/**
* 在写入响应体之前调用此方法进行预处理
* 本实现中不需要任何预处理,因此返回null
*
* @param body 响应体内容
* @param returnType 返回类型
* @param selectedContentType 选定的内容类型
* @param selectedConverterType 选定的HTTP消息转换器类型
* @param request HTTP请求
* @param response HTTP响应
* @return 返回null,表示不需要进行特殊处理
*/
@Override
public Response beforeBodyWrite(Response body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (Objects.nonNull(body) && body.getData() != null) {
String data = JSON.toJSONString(body.getData());
log.info("ResponseBody 明文:" + data);
// 用RSA工具将返回的数据进行加密
data = RSAUtil.encryption(data,publicKey);
log.info("ResponseBody 密文:" + data);
body.setData(data);
}
return body;
}
}
api接口
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import www.three.common.response.Response;
import www.three.components.log.annotations.Log;
import www.three.system.user.model.po.TUser;
/**
* 说明:
*
* @author 张小三
* @create 2024-12-03 17:37
* @verson 1.0.0
*/
@Slf4j
@RestController
@RequestMapping(value = "user")
public class TUserController {
/**
* 说明: 添加
* @author zhangxiaosan
* @create 2024/12/3
* @param
* @return
*/
@Log
@PostMapping(value = "add")
public Response<?> add(@RequestBody TUser user){
log.info("添加用户:{}",user.toString());
return Response.success(user);
}
}
测试
前端请求
使用idea的 HTTP Client 工具 请求案例:
json
POST http://127.0.0.1:8081/three-user/user/add
Content-Type: application/json
{"data":"6944323931323669415876424a653867445233636247553464552b3256466c2b3631586639314675584a6b5068394a6f6b54457235464c4743666e496b5342574a6256564e484b4c49667a6d0d0a6c55696d74444b4778363858465945516f34756d4b6938714f494b732f5157742b6a3235543256737733356c7677465976474463613043454e636376726257502f2f6f3337686c6d6b6e46680d0a38367237467663634c6950586648657135534b31736d6e372b6637667658454d386a79454e794c346b44546c444f367050476e594c793041366d65324a4a6136387251536a616963325a51680d0a6756446369386a743346755a47565377574147332f76306739472f57614e4b3330372b4455467377677536536e79534e71703530635637576a5a38533732654477744d4365587945653638550d0a4976704d4c6e6a303465446c754f506474626f33493578693479724265446a6f356f46557a513d3d"}
响应结果
json
{
"code": 0,
"message": "成功",
"data": "4b637168342f4a7537637956784c7368637a527068314772783139515555506e336e7049474469524a654754464a7242327873545766574a666c346d66427268414b754264306a45454766640d0a5a764a76426e644d77526664613632635a48596a72562b546447597366653667796f6c68616648536f786f6737464575765342686b706473624b4d78574b645145637257655a62615a6e4c670d0a6d79566f467a697a7a5468445153744264627a6d304b42524d54654b6f41347746376d6137427878774c66537159526856484261486f305578734b686f4d78763638434342763959653249320d0a4f557073423545784b354e5a724465416663535a502f4b34516455464d4f33655a335552505067674f714f6d42755a4351654858586c3366334441704448444f5a6b644561354b436e702f330d0a636d6e2f38347771586c62373038344d316479546b4e466778696358592b62555864567a51773d3d",
"count": null
}
扩展
Response响应格式实体
java
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import lombok.Data;
/**
* 说明:
*
* @author 张小三
* @create 2024-07-12 16:12
* @verson 1.0.0
*/
@Data
public class Response<T> {
/**
* 状态码
* 其他值表示失败
*/
private Integer code;
/**
* 提示信息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 数据总量(layui 列表使用)
*/
private Integer count;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Response(Integer code, String message, T data, Integer count) {
this.code = code;
this.message = message;
this.data = data;
this.count = count;
}
public static <T> Response<T> success(){
return response(ResponseCode.successCode,ResponseCode.successMessage,null,null);
}
public static <T> Response<T> success(T data){
return response(ResponseCode.successCode,ResponseCode.successMessage,data,null);
}
public static <T> Response<T> success(T data, String message){
return response(ResponseCode.successCode,message,data,null);
}
public static <T> Response<T> fail(){
return response(ResponseCode.failCode,ResponseCode.failMessage,null,null);
}
public static <T> Response<T> fail(String message){
return response(ResponseCode.failCode,message,null,null);
}
private static <T> Response<T> response(Integer code,String message,T data,Integer count){
return new Response(code,message,data,count);
}
@Override
public String toString() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("code",code);
jsonObject.put("message",message);
jsonObject.put("count",count);
jsonObject.put("data",data);
return JSON.toJSONString(jsonObject, JSONWriter.Feature.WriteNulls);
}
}
ResponseCode 响应状态码
java
import java.io.Serializable;
/**
* 说明:
* 响应状态码
* @author 张小三
* @create 2024-07-12 14:19
* @verson 1.0.0
*/
public class ResponseCode implements Serializable {
/**
* 说明: 成功状态码
* @author zhangxiaosan
* @create 2024/7/12
* @param
* @return
*/
public static final Integer successCode = 0;
/**
* 说明: 成功默认信息
* @author zhangxiaosan
* @create 2024/7/12
* @param
* @return
*/
public static final String successMessage = "成功";
/**
* 说明: 失败状态码
* @author zhangxiaosan
* @create 2024/7/12
* @param
* @return
*/
public static final Integer failCode = -1;
/**
* 说明: 失败默认信息
* @author zhangxiaosan
* @create 2024/7/12
* @param
* @return
*/
public static final String failMessage = "失败";
}
RSA工具类
java
import com.alibaba.fastjson2.JSONObject;
import www.three.common.security.hex.HexUtil;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 说明:
* rsa 工具类
* @author 张小三
* @create 2024-12-03 11:52
* @verson 1.0.0
*/
public class RSAUtil {
private static String keySize = "2048";
/**
* 创建公钥和私钥
*
* @return
* @throws RSAException
*/
public static Map<String, String> createKeys() throws RSAException {
KeyPairGenerator keygen;
try {
keygen = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RSAException("RSA初始化密钥出现错误,算法异常");
}
SecureRandom secrand = new SecureRandom();
//初始化随机产生器
secrand.setSeed("jst".getBytes());
//初始化密钥生成器
if (Objects.isNull(keySize)){
keySize = "2048";
}
keygen.initialize(Integer.valueOf(keySize), secrand);
KeyPair keyPair = keygen.genKeyPair();
//获取公钥并转成base64编码
byte[] pub_key = keyPair.getPublic().getEncoded();
String publicKeyStr = Base64.getEncoder().encodeToString(pub_key);
//获取私钥并转成base64编码
byte[] pri_key = keyPair.getPrivate().getEncoded();
String privateKeyStr = Base64.getEncoder().encodeToString(pri_key);
// 公钥和私钥转成16进制
publicKeyStr = HexUtil.encode(publicKeyStr);
privateKeyStr = HexUtil.encode(privateKeyStr);
//创建一个Map返回结果
Map<String, String> keyPairMap = new HashMap<>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
return keyPairMap;
}
/**
* RSA 加密
*
* @param data 加密内容
* @param key 密钥
* @return
*/
public static String encryption(String data,String key) throws RSAException {
// 16进制密钥还原
key = HexUtil.decode(key);
byte[] publicKeyByte = Base64.getMimeDecoder().decode(key);
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyByte);
String res = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE,publicKey);
byte[] bytes = cipher.doFinal(data.getBytes());
res = Base64.getMimeEncoder().encodeToString(bytes);
res = HexUtil.encode(res); // 密文转成16进制
} catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException |
IllegalBlockSizeException | BadPaddingException e) {
throw new RSAException("RSA出现错误,算法异常:"+e.getMessage());
}
return res ;
}
/**
* RSA 解密
*
* @param data 密文
* @param key 密钥
* @return
*/
public static String decryption(String data ,String key) throws RSAException {
// 16进制密钥还原
key = HexUtil.decode(key);
byte[] privateKeyByte = Base64.getMimeDecoder().decode(key);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyByte);
String res = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE,privateKey);
// 密文还原
data = HexUtil.decode(data);
byte[] decode = Base64.getMimeDecoder().decode(data);
byte[] bytes = cipher.doFinal(decode);
res = new String(bytes);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
throw new RSAException("RSA出现错误,算法异常:"+e.getMessage());
}
return res ;
}
/**
* RSA 签名
*
* @param data 待签名的数据
* @param privateKey 私钥
* @return 签名后的字符串
* @throws RSAException 如果签名过程中出现异常
*/
public static String sign(String data, String privateKey) throws RSAException {
// 16进制密钥还原
privateKey = HexUtil.decode(privateKey);
byte[] privateKeyByte = Base64.getMimeDecoder().decode(privateKey);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyByte);
String signature = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKeyObj = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKeyObj);
signer.update(data.getBytes());
byte[] signedData = signer.sign();
signature = Base64.getMimeEncoder().encodeToString(signedData);
signature = HexUtil.encode(signature); // 签名转成16进制
} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) {
throw new RSAException("RSA签名出现错误,算法异常:" + e.getMessage());
}
return signature;
}
/**
* RSA 验签
*
* @param data 原始数据
* @param publicKey 公钥
* @param signature 签名
* @return 验签结果
* @throws RSAException 如果验签过程中出现异常
*/
public static boolean verify(String data, String publicKey, String signature) throws RSAException {
// 16进制密钥还原
publicKey = HexUtil.decode(publicKey);
byte[] publicKeyByte = Base64.getMimeDecoder().decode(publicKey);
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyByte);
boolean isVerify = false;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKeyObj = keyFactory.generatePublic(x509EncodedKeySpec);
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initVerify(publicKeyObj);
signer.update(data.getBytes());
// 签名还原
signature = HexUtil.decode(signature);
byte[] decodedSignature = Base64.getMimeDecoder().decode(signature);
isVerify = signer.verify(decodedSignature);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) {
throw new RSAException("RSA验签出现错误,算法异常:" + e.getMessage());
}
return isVerify;
}
}
RequestBodyAdvice 介绍
用于在请求体被反序列化之前对其进行处理,典型场景是解密请求体。用于拦截请求体(Request Body)的处理,在请求体被反序列化为 Java 对象之前对其进行处理。这个接口允许开发者在请求进入控制器之前做一些预处理,比如解密或格式转换等。
RequestBodyAdvice 接口提供了 5 个方法,具体如下:
-
boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType)
用于判断当前请求是否需要处理,返回 true 表示该请求体需要进行处理。你可以根据 methodParameter、targetType 或 converterType 来做特定的控制。
-
Object handleBodyBeforeRead(MethodParameter methodParameter, Type targetType, HttpInputMessage inputMessage, Class<? extends HttpMessageConverter<?>> converterType) throws IOException
在请求体反序列化之前调用,可以对请求体进行解密、转换或者其他处理。返回值将是处理后的请求体内容,或者可以直接读取 inputMessage 数据并进行修改。
-
Object handleBodyAfterRead(Object body, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType, HttpInputMessage inputMessage)
请求体被成功反序列化后调用。在此方法中你可以进一步处理数据,比如验证或转换成其他格式。
-
boolean handleBodyAfterWrite(Object body, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType, HttpOutputMessage outputMessage) throws IOException
在响应体被序列化后调用,允许你对响应体进行最后的修改,比如加密、转换等。返回值决定了是否继续执行后续处理。
-
boolean handleBodyBeforeWrite(Object body, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType, HttpOutputMessage outputMessage) throws IOException
在响应体被写入前调用。你可以在这里对响应体数据进行修改、加密等处理。
使用场景
解密请求体: 在请求到达控制器之前,你可能需要对请求体进行解密操作(例如,使用某种加密算法加密请求体数据)。
日志记录: 记录请求的请求体内容,或者修改请求体中的数据。
格式转换: 例如,将请求体从 JSON 转换成 XML,或者做其他格式的处理。
ResponseBodyAdvice 介绍
用于在响应体被序列化并发送之前对其进行处理,典型场景是加密响应体。当控制器方法执行完毕并返回数据后,Spring 会通过 ResponseBodyAdvice 对响应体进行处理。它可以用于对响应数据进行加密、修改、过滤等操作。
ResponseBodyAdvice 接口提供了 2 个方法,具体如下:
- boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType)
用于判断是否对当前的响应体执行处理。你可以根据返回类型、converterType 等条件判断是否处理。 - Object handleBodyBeforeWrite(Object body, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType, HttpOutputMessage outputMessage) throws IOException
在响应体被序列化并写入 HTTP 响应之前调用。你可以在这里对响应体进行加密或修改等操作。
使用场景
加密响应体: 例如,在返回给客户端的数据需要加密时,你可以在 ResponseBodyAdvice 中对响应体进行加密。
日志记录: 记录响应体内容,或者根据需要修改返回的数据。
修改响应体格式: 可以根据需要改变响应的格式,比如将返回的 JSON 数据转换成其他格式。