Spring Boot + Vue 基于RSA+AES的混合加密

目录

一、后端实现

二、前端实现(Vue2)

三、补充

1.增强安全措施

四、最后说明


步骤大致如下:

  1. 后端生成RSA密钥对,提供公钥接口。
  2. 前端请求公钥,生成随机AES密钥和IV。
  3. 用RSA公钥加密AES密钥,用AES密钥加密数据。
  4. 发送包含加密后的AES密钥和数据的请求体。
  5. 后端用RSA私钥解密AES密钥,再用AES密钥解密数据。
  6. 使用注解和拦截器自动处理解密过程。

需要确保每个步骤都正确实现,特别是加密模式、填充方式以及编码解码的一致性,避免因配置不同导致解密失败。有什么没加入的在评论区艾特我,我进行补充

一、后端实现

  • 新增AES工具类:
java 复制代码
public class AesUtils {
	public static String encrypt(String data, String key, String iv) throws Exception {
		SecretKeySpec keySpec = new SecretKeySpec(
				Base64.getDecoder().decode(key), "AES"
		);
		IvParameterSpec ivSpec = new IvParameterSpec(
				Base64.getDecoder().decode(iv)
		);

		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
		cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
		byte[] encrypted = cipher.doFinal(data.getBytes());
		return Base64.getEncoder().encodeToString(encrypted);
	}

	public static String decrypt(String data, String key, String iv) throws Exception {
		byte[] keyBytes = Base64.getDecoder().decode(key);
		byte[] ivBytes = Base64.getDecoder().decode(iv);
		byte[] encryptedBytes = Base64.getDecoder().decode(data);

		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
		SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
		IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

		cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
		return new String(cipher.doFinal(encryptedBytes));
	}
}
  • 修改请求处理切面:
java 复制代码
/**
 * 解密切面
 */
@ControllerAdvice
public class DecryptAdvice extends RequestBodyAdviceAdapter {
	private final RsaKeyManager keyManager;

	public DecryptAdvice(RsaKeyManager keyManager) {
		this.keyManager = keyManager;
	}

	@Override
	public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
		return methodParameter.hasMethodAnnotation(NeedDecrypt.class);
	}

	@Override
	public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
	                                       MethodParameter parameter,
	                                       Type targetType,
	                                       Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
		try {
			String encryptedBody = new String(inputMessage.getBody().readAllBytes());
			JSONObject json = JSONObject.parseObject(encryptedBody);

			// 解密 AES 密钥
			String encryptedAesKey = json.getString("encryptedKey");
			String aesKey = RsaUtils.decryptByPrivateKey(encryptedAesKey, keyManager.getPrivateKey());

			// 解密数据
			String decryptedData = AesUtils.decrypt(
					json.getString("encryptedData"),
					aesKey,
					json.getString("iv")
			);
			return new DecryptedHttpInputMessage(
					new ByteArrayInputStream(decryptedData.getBytes()),
					inputMessage.getHeaders()
			);
		} catch (Exception e) {
			throw new RuntimeException("解密失败", e);
		}
	}
}
  • 新增注解
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedDecrypt {
}
  • 新增公私钥管理类
java 复制代码
/**
 * 生成RSA
 */
public class RsaKeyManager {

	Logger log = LoggerFactory.getLogger(RsaKeyManager.class);

	private final RedisService redisService;

	// Redis 键名
	private static final String PUBLIC_KEY = "rsa:public";
	private static final String PRIVATE_KEY = "rsa:private";

	public RsaKeyManager(RedisService redisService) {
		this.redisService = redisService;
	}
	/**
	 * 初始化密钥(全局唯一)
	 */
	@PostConstruct
	public void initKeyPair() throws Exception {
		// 使用 SETNX 原子操作确保只有一个服务生成密钥
		Boolean isAbsent = redisService.setIfAbsent(PUBLIC_KEY, "");
		if (Boolean.TRUE.equals(isAbsent)) {
			KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
			generator.initialize(2048);
			KeyPair keyPair = generator.generateKeyPair();
			// 存储密钥
			redisService.set(PUBLIC_KEY,
					Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded())
			);
			redisService.set(PRIVATE_KEY,
					Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded())
			);
			log.info("---------------------------初始化RSA秘钥---------------------------");
		}
	}
	/**
	 * 获取公钥
	 */
	public String getPublicKey() {
		Object publicKey = redisService.get(PUBLIC_KEY);
		return Objects.isNull(publicKey)?null:publicKey.toString();
	}

	/**
	 * 获取私钥
	 */
	public String getPrivateKey() {
		Object privateKey = redisService.get(PRIVATE_KEY);
		return Objects.isNull(privateKey)?null:privateKey.toString();
	}
}
  • 新增DecryptedHttpInputMessage
java 复制代码
public class DecryptedHttpInputMessage implements HttpInputMessage {
	private final InputStream body;
	private final HttpHeaders headers;

	public DecryptedHttpInputMessage(InputStream body, HttpHeaders headers) {
		this.body = body;
		this.headers = headers;
	}

	@Override
	public InputStream getBody() throws IOException {
		return this.body;
	}

	@Override
	public HttpHeaders getHeaders() {
		return this.headers;
	}
}
  • 新增获取公钥接口
java 复制代码
@RestController
@RequestMapping("/rsa")
public class RSAController {

	@Autowired
	private RsaKeyManager rsaKeyManager;

	/**
	 * 获取公钥
	 * @return 结果
	 */
	@GetMapping("/publicKey")
	public R<String> getPublicKey() {
		String publicKey = rsaKeyManager.getPublicKey();
		return R.ok(publicKey);
	}

}

二、前端实现(Vue2)

  • 安装新依赖:
java 复制代码
npm install crypto-js
  • 加密工具(src/utils/crypto.js):

getPublicKey 为请求公钥的接口,需要按照自己请求方式去获取

java 复制代码
import JSEncrypt from 'jsencrypt'
import CryptoJS from 'crypto-js'
import { getPublicKey } from '../request/api/auth'

// 初始化公钥
export async function initPublicKey() { 
    try {
        const res = await getPublicKey()
        return formatPublicKey(res.data)
    } catch (error) {
        console.error('公钥获取失败:', error)
        throw new Error('安全模块初始化失败')
    }
}

// 生成AES密钥
export function generateAesKey() {
    const key = CryptoJS.lib.WordArray.random(32)
    const iv = CryptoJS.lib.WordArray.random(16)
    return {
        key: CryptoJS.enc.Base64.stringify(key),
        iv: CryptoJS.enc.Base64.stringify(iv)
    }
}

// AES加密
export function aesEncrypt(data, key, iv) {
    const encrypted = CryptoJS.AES.encrypt(
        JSON.stringify(data),
        CryptoJS.enc.Base64.parse(key), 
        { 
            iv: CryptoJS.enc.Base64.parse(iv),
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        }
    )
    return encrypted.toString()
}

// 格式化公钥
function formatPublicKey(rawKey) {
    return `-----BEGIN PUBLIC KEY-----\n${wrapKey(rawKey)}\n-----END PUBLIC KEY-----`
}

// 每64字符换行
function wrapKey(key) {
    return key.match(/.{1,64}/g).join('\n')
}
  • 修改请求拦截器:
javascript 复制代码
service.interceptors.request.use(async config => {
    if (config.needEncrypt) {
        await initPublicKey(service)
        
        // 生成AES密钥
        const aes = generateAesKey()
        
        // 加密数据
        const encryptedData = aesEncrypt(config.data, aes.key, aes.iv)
        
        // 加密AES密钥
        const encryptor = new JSEncrypt()
        encryptor.setPublicKey(publicKey)
        const encryptedKey = encryptor.encrypt(aes.key)
        
        // 构造请求体
        config.data = {
            encryptedKey: encryptedKey,
            encryptedData: encryptedData,
            iv: aes.iv
        }
    }
    return config
})

三、补充

  • 后端需要加密的接口示例
java 复制代码
@PostMapping("/secure-data")
@NeedDecrypt
public String handleSecureData(@RequestBody Map<String, Object> decryptedData) {
    return "Decrypted data: " + decryptedData.toString();
}
  • 请求结构体
java 复制代码
{
    "encryptedKey": "RSA加密后的AES密钥",
    "encryptedData": "AES加密后的数据",
    "iv": "Base64编码的IV"
}

1.增强安全措施

  • 密钥时效性
java 复制代码
// 前端每次请求生成新密钥
const aes = generateAesKey()
  • 完整性校验
java 复制代码
// 后端解密后可添加HMAC校验
String hmac = json.getString("hmac");
if(!verifyHMAC(decryptedData, hmac, aesKey)) {
    throw new SecurityException("Data tampered");
}
  • 防御重放攻击
java 复制代码
// 前端添加时间戳和随机数
config.data.timestamp = Date.now()
config.data.nonce = Math.random().toString(36).substr(2)

四、最后说明

该方案相比纯RSA加密有以下优势:

  1. 性能提升:AES加密大数据效率比RSA高1000倍以上

  2. 前向安全性:每次请求使用不同AES密钥

  3. 安全性增强:CBC模式+随机IV避免模式分析攻击

实际部署时需注意:

  1. 使用HTTPS传输加密后的数据

  2. 定期轮换RSA密钥对

  3. 对敏感接口添加频率限制

  4. 在网关层实现解密拦截器(而非应用层)

相关推荐
程序员码歌10 分钟前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端
java坤坤1 小时前
GoLand 项目从 0 到 1:第八天 ——GORM 命名策略陷阱与 Go 项目启动慢问题攻坚
开发语言·后端·golang
元清加油1 小时前
【Golang】:函数和包
服务器·开发语言·网络·后端·网络协议·golang
一枚小小程序员哈1 小时前
基于Vue + Node能源采购系统的设计与实现/基于express的能源管理系统#node.js
vue.js·node.js·express
bobz9652 小时前
GPT-4.1 对比 GPT-4o
后端
Java小白程序员2 小时前
Spring Framework :IoC 容器的原理与实践
java·后端·spring
小小愿望2 小时前
前端无法获取响应头(如 Content-Disposition)的原因与解决方案
前端·后端
追逐时光者3 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 50 期(2025年8.11-8.17)
后端·.net
杨DaB4 小时前
【SpringBoot】Swagger 接口工具
java·spring boot·后端·restful·swagger
why技术4 小时前
也是震惊到我了!家里有密码锁的注意了,这真不是 BUG,是 feature。
后端·面试