今天的问题:
个人在是使用RSA进行前后端加密传输,前端对整个传输的body进行加密,
后端接口接收的是一个User对象,包含username、password,
个人预期是在aop内将其解密成user对象,然后controller就可以正常接收,但是请求根本发送不出去:Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of
com.lin.me.entity.User
(although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('IZlPsCd2qVBL0hGVTzb23owpvyXa5xom1z5YXBCbrdlUXS...................................................意思是必须要接收一个User对象 请问大家怎么解决
确实,传送明文数据就像把数据暴露给所有人一样,因此为了数据安全,加密是必不可少的。
要实现上面的功能,即在Spring Boot应用中通过AOP(面向切面编程)对前端使用RSA加密的请求体进行解密,然后将解密后的数据自动转换为User对象供Controller方法使用,需要几个步骤来完成。下面是一个简化的示例,帮助您理解如何实现这一流程。
步骤 1: 添加依赖
确保您的项目中包含了Spring AOP和RSA加密相关的依赖。如果是Maven项目,可以在pom.xml
中添加如下依赖:
XML
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- RSA 加密库,这里以Bouncy Castle为例 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
步骤 2: 定义User类
假设您已有User类定义,大致如下:
java
public class User {
private String username;
private String password;
// getters and setters
}
步骤 3: 实现RSA加解密工具类
创建一个工具类用于RSA的加解密操作。这里简化处理,实际应用中应更安全地管理密钥。
java
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.*;
import javax.crypto.Cipher;
public class RSAUtil {
static {
Security.addProvider(new BouncyCastleProvider());
}
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
public static byte[] decrypt(byte[] encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedData);
}
}
步骤 4: 创建AOP切面进行解密
创建一个AOP切面,用于拦截Controller中的方法,并对加密的请求体进行解密。
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@Aspect
@Component
public class DecryptRequestBodyAspect {
@Around("@annotation(decryptRequestBody)")
public Object decryptRequestBody(ProceedingJoinPoint joinPoint, DecryptRequestBody decryptRequestBody) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestBody = readRequestBody(request);
if (requestBody != null) {
requestBody = decrypt(requestBody); // 假设decrypt方法实现了从字符串解密到User对象的逻辑
// 将解密后的请求体设置回请求中(如果需要)
// request.setAttribute("BODY", requestBody);
}
return joinPoint.proceed();
}
private String readRequestBody(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
char[] buff = new char[1024];
int len;
while ((len = reader.read(buff)) != -1) {
sb.append(buff, 0, len);
}
}
return sb.toString();
}
// 假设的解密方法,实际需要根据加密方式实现
private String decrypt(String encryptedBody) {
// 这里需要实现从加密字符串到User对象的具体逻辑
// 包括从请求中获取或配置中读取私钥等操作
return ""; // 返回解密后的User对象的JSON字符串或其他形式
}
}
步骤 5: 自定义注解标记需要解密的方法
创建一个自定义注解,用于标记那些需要解密请求体的方法。
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptRequestBody {
}
步骤 6: 在Controller中使用
最后,在您的Controller方法上使用这个自定义注解,以指示AOP切面进行解密操作。
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping("/login")
@DecryptRequestBody
public ResponseEntity<?> login(@RequestBody User user) {
// 这时user对象应该是解密后的结果
// 实现登录逻辑...
}
}
请注意,这里的实现非常简化,实际应用中还需要考虑加密算法的安全性、异常处理、性能优化(如加密大文本时可能需要分块加密和解密)、私钥的安全存储等问题。同时,直接在请求体中加密整个User对象(特别是密码)可能不是最佳实践,通常密码应该单独加密处理,且不建议明文传输密码。
步骤 7: 前端js加密
在前端使用RSA加密,一个常用的库是jsencrypt
,它提供了简单的API来实现RSA公钥加密。以下是如何使用jsencrypt
库进行RSA加密的示例代码:
首先,确保你已经在HTML文件中引入了jsencrypt.js
库。你可以通过CDN或者下载到本地项目中引用。
javascript
<script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.3.2/jsencrypt.min.js"></script>
然后在JavaScript中使用以下代码进行加密:
javascript
// 假设后端已经提供了公钥,并且你已将其存储在变量publicKey中
const publicKey = "-----BEGIN PUBLIC KEY-----\nYOUR_PUBLIC_KEY_HERE\n-----END PUBLIC KEY-----";
// 创建JSEncrypt实例
const encryptor = new JSEncrypt();
// 设置公钥
encryptor.setPublicKey(publicKey);
// 需要加密的数据,例如用户密码
const plaintext = "your_plain_text_here";
// 执行加密操作
const ciphertext = encryptor.encrypt(plaintext);
console.log("Encrypted Text:", ciphertext);
请将YOUR_PUBLIC_KEY_HERE
替换为实际的公钥字符串。这段代码会使用公钥对plaintext
中的数据进行加密,并将加密后的结果输出到控制台。
请知悉,RSA加密适合加密小块数据,如密码或一些敏感信息的摘要,因为加密大量数据可能会导致性能问题。对于大块数据,可以考虑结合对称加密算法(如AES)和RSA来加密数据和密钥。
后端公钥和私钥生成类GenerateRSAKeys:
java
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
public class GenerateRSAKeys {
public static void main(String[] args) throws Exception {
// 获取KeyPairGenerator实例
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器,可以指定密钥长度,例如2048位
keyPairGenerator.initialize(2048);
// 生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 获取公钥和私钥
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 打印公钥和私钥(这里为了演示,简单打印其Base64编码形式)
System.out.println("Public Key: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
System.out.println("Private Key: " + Base64.getEncoder().encodeToString(privateKey.getEncoded()));
}
}
后端加解密工具类RSAUtil:
java
package com.example.default_setting.util;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.Cipher;
public class RSAUtil {
// 示例RSA密钥,实际使用时请从安全的地方加载
private static final String PUBLIC_KEY = "YOUR_PUBLIC_KEY";
private static final String PRIVATE_KEY = "YOUR_PRIVATE_KEY";
// 初始化RSA密钥对
private static PublicKey publicKey = null;
private static PrivateKey privateKey = null;
static {
Security.addProvider(new BouncyCastleProvider());
init();
}
public static void init() {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(PUBLIC_KEY)));
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIVATE_KEY)));
} catch (Exception e) {
throw new RuntimeException("Failed to initialize RSA keys", e);
}
}
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
public static byte[] decrypt(byte[] encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedData);
}
public static byte[] decrypt(byte[] encryptedData) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedData);
}
public static void main(String[] args) {
System.out.println(1);
}
}
但是还是不能整体加密后给后端使用,可能的解决办法如下:
1.局部加密
2.后端全部用String接收
3.后端实体增加一个字段接收,然后再在aop界面填充
如果大家还发现更好的办法,可以评论区说一下,感谢阅读!