今天的问题:
个人在是使用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中添加如下依赖:
org.springframework.boot spring-boot-starter-aoporg.bouncycastle bcprov-jdk15on1.70
步骤 2: 定义User类
假设您已有User类定义,大致如下:
public class User { private String username; private String password; // getters and setters }
步骤 3: 实现RSA加解密工具类
创建一个工具类用于RSA的加解密操作。这里简化处理,实际应用中应更安全地管理密钥。
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中的方法,并对加密的请求体进行解密。
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: 自定义注解标记需要解密的方法
创建一个自定义注解,用于标记那些需要解密请求体的方法。
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切面进行解密操作。
@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中使用以下代码进行加密:
// 假设后端已经提供了公钥,并且你已将其存储在变量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:
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:
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界面填充
如果大家还发现更好的办法,可以评论区说一下,感谢阅读!
还没有评论,来说两句吧...