springboot+vue接口加密:RSA+AES
发表于湖南。
程序员黑哥 2023-12-15 17:06 发表于湖南
0. 写在之前
文章写于2022年12月26日,从不查验健康码开始,到明天确定新冠肺炎改名为新冠感染,疫情放开大约已有两周,上周项目负责人也不幸被感染,高烧居家。之前开发完接口加密后,项目的对接到目前仍被搁置。
原来的项目使用的是:根据服务名、用户标识、时间戳等信息,使用前后端(C/S)约定好的公共密钥对其进行加签生成一个sign,然后将这些信息拼接到请求url中。此种方式只能证明该请求是合法的,并不能对信息进行加密。
项目负责人告诉我客户想换一种接口加密的方式,于是我自然是在网络上借鉴了一些文章,经过自己的总结与测试,提出了这个方案。但方案还得客户验收,还没给回复,不知道他们行不行,反正我这能跑。
最近可能是有点闲了,也不知道这方案最后是否能落地,先写一篇文章记录一下吧!
1. 整体预览
-
整体思想为:
先使用AES对数据进行加密,再使用RSA对AES密钥进行加密。
-
至于为什么要使用这种方式呢?总结起来大致就是又安全速度又快。
markdown
复制代码
1. RSA算法复杂,比较耗时,但比较安全
2. AES密钥固定,双方使用同一密钥,但速度快效率高
3. 所以用AES密钥加密数据,RSA加密AES密钥,形成混合加密
-
至于RSA和AES是什么?文章篇幅有限,还请大家到网上自行了解。
-
接下来是实现加密的具体流程。
请求:
客户端发起请求时,客户端使用随机生成的AES密钥对数据进行加密
使用服务器的 公钥 对AES密钥进行加密
将 加密后的数据和AES密钥 作为请求数据发送至服务器。
服务器收到请求后,使用服务器的 私钥 对加密过的AES密钥进行解密
再使用获取到的AES密钥对加密的数据进行解密。
以此保证客户端发送的数据只能被服务器解密进行处理。
应该是有个图,有人看的话后面再补吧
响应:
服务器处理完数据后,将响应数据先使用随机生成(新)的AES密钥进行加密
使用服务器的 私钥 对AES密钥进行签名(签名算法为[ MD5withRSA ])
将 加密后的响应数据、AES密钥、签名 作为响应数据发送至客户端。
客户端收到响应后,先使用服务器的 公钥 对签名过的AES密钥进行验证
验签成功后再使用的AES密钥对加密的响应数据进行解密。
以此保证客户端收到的响应数据为合法服务器返回的。
2. 加密方式
加密算法: AES
KEY长度: 16 * 8
加密模式: ECB
数据填充方式: PKCS5Padding
加密算法: RSA
RSA位数: 2048
加密模式: ECB
数据填充方式: PKCS1Padding
3. 例
-
需要加密的数据:
{"staffUid": "zs"} -
生成的AES密钥:
1@HLKMUCHkywdrel -
数据加密后:
1qQL/+ufmvg124o5lc/IvzNVKsJniWomAaTrwWLX/4E= -
服务器公钥:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnYnyTKWgBrrPeP/5D0w8Ts5aSiJKOgZ2TV9WkTDLwnE4nHpKxkE80uEP0jocJYboZ2wY+M9w4U01RBc7V5uqI4w46EoD0t3oBWkymKYsOfs0FLdPsqeGc7wUBkysJ3oGJcv3DRBXUrnJo3LJbxpOW/34CdGsLpfND6rAQFnR6oQNWUUskZYobsbhNSO7MGp0FSZNnXdz9ahS2QAHQWkycBZfqx6iLqA701LTsBrA4cAWTMxmlKrCSUCwZb85hrK7fCNkS8NMqpMLuN466TQgfZ/umFD38LwdiW2k/5glxNkhGsrjFmPzEnGqYhhIG1BJ3npdX/dhU69D3r91g8Hf4wIDAQAB
-
使用公钥加密后的AES密钥:
ihPxFWDmLPDw5VUXu4ClbFBZxy92ZKhp4E8ypl7uM/9gWJpKPMcMPk1GERPKmzD24Rx06v3ROuo5Dj1Lryc1XVjvvSC9Z6Jq7fmCtMUjQJW/7PdXgO9PQmId/3J2q5NtWPutyyuyTAz5J+tDPGKUlzGvO5vcxQVJXj4O3kBYZqZu8yVqbtx23zeXclQPmtmJYxNaZO84eJb1HymlngbmDKgDtH0+UyRM9fgksDDBbI6gfeRvSOdUmlD/oTqnnS0AFzucjnnNFeugeoRlE1sXnKuv47ePpJDLzgX+XHZjwhWhMLmNLZscE0IL1Ue4JUdRmzKc3Xb3SUu+/b6zBiHl6w==
-
发送的请求数据:
data: 1qQL/+ufmvg124o5lc/IvzNVKsJniWomAaTrwWLX/4E=aesKey: ihPxFWDmLPDw5VUXu4ClbFBZxy92ZKhp4E8ypl7uM/9gWJpKPMcMPk1GERPKmzD24Rx06v3ROuo5Dj1Lryc1XVjvvSC9Z6Jq7fmCtMUjQJW/7PdXgO9PQmId/3J2q5NtWPutyyuyTAz5J+tDPGKUlzGvO5vcxQVJXj4O3kBYZqZu8yVqbtx23zeXclQPmtmJYxNaZO84eJb1HymlngbmDKgDtH0+UyRM9fgksDDBbI6gfeRvSOdUmlD/oTqnnS0AFzucjnnNFeugeoRlE1sXnKuv47ePpJDLzgX+XHZjwhWhMLmNLZscE0IL1Ue4JUdRmzKc3Xb3SUu+/b6zBiHl6w==
-
响应结果如下:
-
{"code": 200,"message": "操作成功","data": {"data": "zKpHLUQ9NtvRizSUwdEtuM5pMOWf8qBGmGuW1ZYqGeQMw/00i4wwhXMCS0MoabdJ4zP6ETdZip02xq9sb/4EtRvLFWEqoVOs4MXtFV8QYiWsMxw1jbmRQmWCc8WBlqtQVkLyQfdQw51LIapbLtfrZGXhryqSHZhil6RUHU4RuXqwUPBMzlsobquhxKMNePJ6hGcEi5Z+JIcAeUNVWCP9hrfNPl/zYNrf4WWvmCW/tnBS2ffg34Unqho+iywEW+46bUfbgnE34LgeZqCNMG7MfyNY+DHueaWcwcPekNzzbdmawWUfs4miTUkh7Uz7Fm7+AdrmM/ZCgIqwene1LpYZVaJyHgLwlLX31gzXVeQxYpdshnbz+x6Rmf81xS+gysVrQywXVY13/QWz+mb9jmPqcS04vHf9sbA0pZ7UjUMVe+3mXK4mUdL11eq+Ti+ZvqnEIYKkOxKrOJWoQ1mKT86cMoYcS18Fu8+oTgRch2S1QZrEVApnoHBvjJXsgsl4PRIHWZZEPGhFAohK0fXQXr7JQQLee8ALpxVEd1RyFOevoBnO/Dt5MeKsmKNBK7HMQpxB","aesKey": "U85n3MtPMlmPTwLs","sign": "F6mQbV40E9SbuV/m+jIH+VDCqtxFOhzO10XLCGZoZ2b8We5a1BSXPum8S+7oAu/JaqfYi3fPFjzLHCVu+uWhSMhch3m1IRFOpZp7Pa04AawbUv0MuyrtyBqgNjLxJna5FhTX4NbOc4VaT1gvLO3wi4KWuZ5Ymp1ALWDW1QrmJkYzKasp6lsVyovYuj6GWESykdlmskzdI6iECSXtRatb3NpIe2vc9By8sXd8VytbcNlTHQHcidnuQNprYexp53IpbMRNma9zpqM+iqzX+PWUYlaSMLxeUEskLT3h/V4YhMj2vorrf1HM+nyGxO2gR+sVQnb8sy7caJy8c/2m12AlwA=="}}
-
响应解密后:
{"code": 200,"message": "操作成功","data": {"id": 1,"staffUid": "zs","staffPassword": null,"staffName": "张三","staffCardId": "123","staffLoginName": "zszs","staffEmail": "abc@123.com","staffPhone": "13696365412","staffAdmin": 1,"staffStatus": 0,"staffDepartment": "IT","staffCode": "qwer","remark": null,"createdBy": "system","createdTime": "2022-10-27 14:14:50","updatedBy": "zs","updatedTime": "2022-11-21 17:54:22","status": 0}}
4. 代码
前端
-
在何处使用?某个vue文件中,将参数通过方法加密后传入请求,待请求结束后,对响应数据使用对应的方法验签解密。
// 测试用例 此处没有逻辑关系loginForm: {staffUid: ''}// get请求和post请求不同,get为普通传参,post需要使用表单传参,至于原因后面会解释// get举例getStaffInfo(resEncryptionForGet(this.loginForm)).then((response) => {console.log(resDecryption(response.data))}).catch(() => {})// post举例testLogin(resEncryption({token: this.loginForm.staffUid})).then((response) => {console.log(resDecryption(response.data))}).catch(() => {})
-
请求长什么样子?某个js文件中,通过封装的request向后端发送axios请求。此处的request与普通的并无二致,包括axios的创建,request和response拦截器,对请求及响应进行统一处理,本文不再列出。
import request from "@/utils/request";export function getStaffInfo(params) {return request({url: '/api/staff/getLoginStaffInfo',method: 'get',params: params})}export function testLogin(params) {return request({url: '/api/staff/login',method: 'post',data: params,// 使用表单传参大致是需要设置headers的headers: {'Content-Type': 'application/x-www-form-urlencoded'}})}
-
加解密方法长什么样子?某个js文件中,包括随机生成AES的密钥、AES加解密、公钥加密(和验签)等方法。较为复杂,还请读者自行消化。
import {b64tohex, KJUR} from 'jsrsasign' // base64转16进制 验签所需插件import CryptoJS from 'crypto-js' // AESimport forge from 'node-forge' // RSAimport request from "./request";import store from "../store";// 客户端(前端)需要获取服务端(后端)的公钥,此处使用接口获取,可以不通过接口获取request({url: '/api/staff/getPublicKey',method: 'get'}).then(result => {// console.log(""+result.data)store.state.user.serverPublicKey = result.data;})// 生成AES密钥function getmm(num = 16) {var amm = ['!', '@', '#', '$', '%', '&', '*', '(', ')', '_', 1, 2, 3, 4, 5, 6, 7, 8, 9]var tmp = Math.floor(Math.random() * num)var s = tmps = s + amm[tmp]for (let i = 0; i < Math.floor(num / 2) - 1; i++) {tmp = Math.floor(Math.random() * 26)s = s + String.fromCharCode(65 + tmp)}for (let i = 0; i < (num - Math.floor(num / 2) - 1); i++) {tmp = Math.floor(Math.random() * 26)s = s + String.fromCharCode(97 + tmp)}return s}// 生成客户端RSA公钥和私钥 需要双向加密时需要,将在文末讨论// export function jsrsasignFn() {// var rsaKeypair = jsrsasign.KEYUTIL.generateKeypair('RSA', 2048)// var private1 = jsrsasign.KEYUTIL.getPEM(rsaKeypair.prvKeyObj, 'PKCS1PRV')// var public1 = jsrsasign.KEYUTIL.getPEM(rsaKeypair.pubKeyObj)// let a = {// 'privateKey': private1.substring(31, private1.length - 31).replace(/\r\n/g, ''),// 'publicKey': public1.substring(28, public1.length - 28).replace(/\r\n/g, '')// }// console.log(a)// return a// }// AES 加密 data:要加密解密的数据,AES_KEY:密钥,function encrypt(data, AES_KEY) {const key = CryptoJS.enc.Utf8.parse(AES_KEY)const encrypted = CryptoJS.AES.encrypt(data, key, {mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7})// console.log("加密数据:" + data)// console.log("数据加密后:" + encrypted.toString())return encrypted.toString()}// 使用服务端的公钥加密AES密钥function pubencrypt(aeskey, pubencryptKey) {// console.log("aeskey:" + aeskey)// console.log("服务器公钥:" + pubencryptKey)// 此处的前后缀不能改变const publicKeyAll = '-----BEGIN PUBLIC KEY-----\n' + pubencryptKey + '\n-----END PUBLIC KEY-----'var publicKey = forge.pki.publicKeyFromPem(publicKeyAll)var buffer = forge.util.createBuffer(aeskey, 'utf8')var bytes = buffer.getBytes()// console.log("公钥加密后的AES密钥:" + a)return forge.util.encode64(publicKey.encrypt(bytes, 'RSAES-PKCS1-V1_5'))}// post请求参数加密export function resEncryption(data) {// 生成随机密钥(key)var key = getmm()// 密钥 加密 数据(将json转成字符串再进行加密)var newData = encrypt(JSON.stringify(data), key)// console.log("--------------------"+newData)// 公钥 加密 密钥(key组成)var aesKey = pubencrypt(key, store.state.user.serverPublicKey)// console.log(aesKey)// 返回数据let formData = new FormData();formData.append('data', newData)formData.append('aesKey', aesKey)return formData}// get请求参数加密export function resEncryptionForGet(data) {// 生成随机密钥(key)var key = getmm()// 密钥 加密 数据(将json转成字符串再进行加密)var newData = encrypt(JSON.stringify(data), key)console.log("--------------------"+newData)// 公钥 加密 密钥(key组成)var aesKey = pubencrypt(key, store.state.user.serverPublicKey)// console.log(aesKey)// 返回数据return {'data': newData,'aesKey': aesKey}}// 使用服务端的公钥验签AES密钥function pubverify(aeskey, pubencryptKey, sign) {// console.log("aeskey:" + aeskey)// console.log("服务器公钥:" + pubencryptKey)const publicKeyAll = '-----BEGIN PUBLIC KEY-----\n' + pubencryptKey + '\n-----END PUBLIC KEY-----'try {let sig = new KJUR.crypto.Signature({alg: "MD5withRSA"});sig.init(publicKeyAll)sig.updateString(aeskey)return sig.verify(b64tohex(sign))} catch (e) {console.log(e)}}// AES 加密 data:要加密解密的数据,AES_KEY:密钥,function aesDecrypt(data, AES_KEY) {// console.log("---------------------开始解密AES")const decrypted = CryptoJS.AES.decrypt(data.toString(), CryptoJS.enc.Utf8.parse(AES_KEY), {mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7})// console.log("数据:" + data.toString())// console.log("解密后:" + decrypted.toString(CryptoJS.enc.Utf8))return decrypted.toString(CryptoJS.enc.Utf8)}// 响应解密export function resDecryption(data) {// 使用公钥先验签aesKeylet verifyResult = pubverify(data.aesKey, store.state.user.serverPublicKey, data.sign);if (!verifyResult) {this.$message({type: 'warning',message: '响应数据不合法!'});return ;}console.log(verifyResult)// 使用aesKey解密数据return aesDecrypt(data.data, data.aesKey);}
后端
-
所需哪些依赖?包括接口加密与base64转换的依赖。
<!--接口加密--><dependency><groupId>org.bouncycastle</groupId><artifactId>bcpkix-jdk15on</artifactId><version>1.56</version></dependency><!-- Base64--><dependency><groupId>org.apache.directory.studio</groupId><artifactId>org.apache.commons.codec</artifactId><version>1.8</version></dependency>
-
如何使用加解密?定义两个用于加解密的注解,使用时在需要加解密的接口方法上进行注解。
// Encrypt.java@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface Encrypt {}// Decrypt.java@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface Decrypt {}
-
AES与RSA如何实现加解密?使用定义的静态工具类,也请读者自行消化。网上关于后端部分的内容较多,作者在此处为直接借鉴。
public class AESUtil {/*** 加密算法AES*/private static final String KEY_ALGORITHM = "AES";/*** key的长度,Wrong key size: must be equal to 128, 192 or 256* 传入时需要16、24、36*/private static final Integer KEY_LENGTH = 16 * 8;/*** 算法名称/加密模式/数据填充方式* 默认:AES/ECB/PKCS5Padding*/private static final String ALGORITHMS = "AES/ECB/PKCS5Padding";/*** 后端AES的key,由静态代码块赋值*/public static String key;/*** 不能在代码中创建* JceSecurity.getVerificationResult 会将其put进 private static final Map<Provider,Object>中,导致内存缓便被耗尽*/private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();static {key = getKey();}/*** 获取key*/public static String getKey() {StringBuilder uid = new StringBuilder();//产生16位的强随机数Random rd = new SecureRandom();for (int i = 0; i < KEY_LENGTH / 8; i++) {//产生0-2的3位随机数int type = rd.nextInt(3);switch (type) {case 0://0-9的随机数uid.append(rd.nextInt(10));break;case 1://ASCII在65-90之间为大写,获取大写随机uid.append((char) (rd.nextInt(25) + 65));break;case 2://ASCII在97-122之间为小写,获取小写随机uid.append((char) (rd.nextInt(25) + 97));break;default:break;}}return uid.toString();}/*** 加密* @param content 加密的字符串* @param encryptKey key值*/public static String encrypt(String content, String encryptKey) throws Exception {//设置Cipher对象Cipher cipher = Cipher.getInstance(ALGORITHMS, PROVIDER);cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), KEY_ALGORITHM));//调用doFinal 转base64return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));}/*** 解密* @param encryptStr 解密的字符串* @param decryptKey 解密的key值*/public static String decrypt(String encryptStr, String decryptKey) throws Exception {//base64格式的key字符串转bytebyte[] decodeBase64 = Base64.decodeBase64(encryptStr);// byte[] a = new String(decodeBase64).getBytes(StandardCharsets.UTF_8);//设置Cipher对象Cipher cipher = Cipher.getInstance(ALGORITHMS,PROVIDER);cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(StandardCharsets.UTF_8), KEY_ALGORITHM));//调用doFinal解密return new String(cipher.doFinal(decodeBase64));}}
@Slf4jpublic class RSAUtil {/*** 加密算法RSA*/private static final String KEY_ALGORITHM = "RSA";/*** 算法名称/加密模式/数据填充方式* 默认:RSA/ECB/PKCS1Padding*/private static final String ALGORITHMS = "RSA/ECB/PKCS1Padding";/*** Map获取公钥的key*/private static final String PUBLIC_KEY = "publicKey";/*** Map获取私钥的key*/private static final String PRIVATE_KEY = "privateKey";/*** RSA最大加密明文大小*/private static final int MAX_ENCRYPT_BLOCK = 245;/*** RSA最大解密密文大小*/private static final int MAX_DECRYPT_BLOCK = 256;/*** 1024 117 128* RSA 位数 如果采用2048 上面最大加密和最大解密则须填写: 245 256*/private static final int INITIALIZE_LENGTH = 2048;/*** 后端RSA的密钥对(公钥和私钥)Map,由静态代码块赋值*/private static Map<String, Object> genKeyPair = new LinkedHashMap<>();static {try {genKeyPair.putAll(genKeyPair());} catch (Exception e) {// 输出到日志文件中log.error(e.getMessage());// System.err.println(e.getMessage());}}/*** 生成密钥对(公钥和私钥)*/private static Map<String, Object> genKeyPair() throws Exception {log.info("-------------------开始生成密钥对");KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);keyPairGen.initialize(INITIALIZE_LENGTH);KeyPair keyPair = keyPairGen.generateKeyPair();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();Map<String, Object> keyMap = new HashMap<>(2);//公钥keyMap.put(PUBLIC_KEY, publicKey);//私钥keyMap.put(PRIVATE_KEY, privateKey);return keyMap;}/*** 私钥解密* @param encryptedData 已加密数据* @param privateKey 私钥(BASE64编码)*/public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) throws Exception {//base64格式的key字符串转Key对象Key privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));Cipher cipher = Cipher.getInstance(ALGORITHMS);cipher.init(Cipher.DECRYPT_MODE, privateK);//分段进行解密操作return encryptAndDecryptOfSubsection(encryptedData, cipher, MAX_DECRYPT_BLOCK);}/*** 公钥加密* @param data 源数据* @param publicKey 公钥(BASE64编码)*/public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception {//base64格式的key字符串转Key对象Key publicK = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(publicKey)));Cipher cipher = Cipher.getInstance(ALGORITHMS);cipher.init(Cipher.ENCRYPT_MODE, publicK);//分段进行加密操作return encryptAndDecryptOfSubsection(data, cipher, MAX_ENCRYPT_BLOCK);}/*** 私钥加密* @param data 源数据* @param privateKey 私钥(BASE64编码)*/public static byte[] encryptByPrivateKey(byte[] data, String privateKey) throws Exception {Key privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));Cipher cipher = Cipher.getInstance(ALGORITHMS);cipher.init(Cipher.ENCRYPT_MODE, privateK);return encryptAndDecryptOfSubsection(data, cipher, MAX_ENCRYPT_BLOCK);}/*** 公钥解密* @param encryptedData 已加密数据* @param publicKey 公钥(BASE64编码)*/public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey) throws Exception {Key publicK = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(publicKey)));Cipher cipher = Cipher.getInstance(ALGORITHMS);cipher.init(Cipher.ENCRYPT_MODE, publicK);return encryptAndDecryptOfSubsection(encryptedData, cipher, MAX_ENCRYPT_BLOCK);}/*** 获取私钥*/public static String getPrivateKey() {Key key = (Key) genKeyPair.get(PRIVATE_KEY);return Base64.encodeBase64String(key.getEncoded());}/*** 获取公钥*/public static String getPublicKey() {Key key = (Key) genKeyPair.get(PUBLIC_KEY);return Base64.encodeBase64String(key.getEncoded());}/*** 分段进行加密、解密操作*/private static byte[] encryptAndDecryptOfSubsection(byte[] data, Cipher cipher, int encryptBlock) throws Exception {int inputLen = data.length;ByteArrayOutputStream out = new ByteArrayOutputStream();int offSet = 0;byte[] cache;int i = 0;// 对数据分段加密while (inputLen - offSet > 0) {if (inputLen - offSet > encryptBlock) {cache = cipher.doFinal(data, offSet, encryptBlock);} else {cache = cipher.doFinal(data, offSet, inputLen - offSet);}out.write(cache, 0, cache.length);i++;offSet = i * encryptBlock;}out.close();return out.toByteArray();}/*** 用私钥对信息生成数字签名* @param data 已加密数据* @param privateKey 私钥(BASE64编码)*/public static String sign(byte[] data, String privateKey) throws Exception {byte[] keyBytes = Base64.decodeBase64(privateKey);PrivateKey privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));Signature signature = Signature.getInstance("MD5withRSA");signature.initSign(privateK);signature.update(data);return Base64.encodeBase64String(signature.sign());}/*** 校验数字签名* @param data 已加密数据* @param publicKey 公钥(BASE64编码)* @param sign 数字签名*/public static boolean verify(byte[] data, String publicKey, String sign) throws Exception {byte[] keyBytes = Base64.decodeBase64(publicKey);PublicKey publicK = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(keyBytes));Signature signature = Signature.getInstance("MD5withRSA");signature.initVerify(publicK);signature.update(data);return signature.verify(Base64.decodeBase64(sign));}}
-
加解密具体在哪里实现?通过spring特性aop的环绕通知,找到切入点下拥有前面定义的加解密注解的接口,对数据进行处理。
@Slf4j@Aspect@Componentpublic class SafetyAspect {/*** Pointcut 切入点* 匹配* cn.huanzi.qch.baseadmin.sys.*.controller、* cn.huanzi.qch.baseadmin.*.controller包下面的所有方法*/@Pointcut(value = "execution(public * com.sy.order.modules.api.controller..*.*(..))")public void safetyAspect() {}/*** 环绕通知*/@Around(value = "safetyAspect()")public Object around(ProceedingJoinPoint pjp) {try {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();assert attributes != null;// request对象HttpServletRequest request = attributes.getRequest();// http请求方法 post getString httpMethod = request.getMethod().toLowerCase();// method方法Method method = ((MethodSignature) pjp.getSignature()).getMethod();// method方法上面的注解Annotation[] annotations = method.getAnnotations();// 方法的形参参数Object[] args = pjp.getArgs();// 是否有@Decryptboolean hasDecrypt = false;// 是否有@Encryptboolean hasEncrypt = false;for (Annotation annotation : annotations) {hasDecrypt = annotation.annotationType() == Decrypt.class;hasEncrypt = annotation.annotationType() == Encrypt.class;}ObjectMapper mapper = new ObjectMapper();// jackson 序列化和反序列化 date处理mapper.setDateFormat( new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));// 执行方法之前解密if (hasDecrypt) {// 此处填坑,后端是用如下的方式获取前端所传参数的,get请求没有问题,但post请求获取body参数时就涉及到request流只能读一次的问题,为了能获取到参数规定前端post请求时使用表单传参,有要求的可自行修改。// AES加密后的数据String dataTmp = request.getParameter("data");log.debug("前端数据:[{}]", dataTmp);// 后端RSA公钥加密后的AES的keyString aesKey = request.getParameter("aesKey");log.debug("AES的加密key:[{}]", aesKey);// 后端私钥解密的到AES的keybyte[] plaintext = RSAUtil.decryptByPrivateKey(Base64.decodeBase64(aesKey), RSAUtil.getPrivateKey());aesKey = new String(plaintext);log.debug("解密出来的AES的key:[{}]", aesKey);// AES解密得到明文data数据String decrypt = AESUtil.decrypt(dataTmp, aesKey);log.debug("解密出来的data数据:[{}]", decrypt);// 设置到方法的形参中,目前只能设置只有一个参数的情况mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);if(args.length > 0){args[0] = mapper.readValue(decrypt, args[0].getClass());}}// 执行并替换最新形参参数 method方法要public修饰的才能设置值Object o = pjp.proceed(args);// 返回结果之前加签名if (hasEncrypt) {mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);// 每次响应之前随机获取AES的key,加密data数据String key = AESUtil.getKey();log.debug("本次响应加密前AES key:[{}]", key);String dataString = mapper.writeValueAsString(o);log.debug("需要加密的data数据:[{}]", dataString);String data = AESUtil.encrypt(dataString, key);log.debug("加密后的data数据:[{}]", data);// 对key 用私钥加签String sign = RSAUtil.sign(key.getBytes(), RSAUtil.getPrivateKey());// 转json字符串并转成Object对象,并赋值给返回值oo = CommonResult.success(mapper.readValue("{"data":"" + data + "","aesKey":"" + key + "","sign":"" + sign + ""}", Object.class));}return o;} catch (Throwable e) {log.error(e.getMessage());// CommonResult为后端统一返回类return CommonResult.failed(ResultCode.REQUEST_FAIL, e.getMessage());}}}
5. 注意
客户端post请求数据统一为FormData格式
客户端需要先获取服务器公钥,本文使用接口获取
数据传输时使用base64编码,前端部分插件数据处理完时即为base64,需看情况处理
6. 写在之后
文章中没有什么有技术含量的东西,但整理出来还是花了两天时间,写出来只是为了记录一下,记录当初的不容易,虽然是左缝右补的。感觉不像个开发工程师,像是个裁缝师。
尽管如此,完成该方案后,我仍有两个问题:
在发送请求阶段,可以保证客户端的数据加密后只能被服务端解密;但在返回响应阶段,客户端只能验证该消息的签名保证其是合法服务端返回的,不能保证该信息没有被他人截取(只要中间人不改变信息则不会被发现)。
若要对返回信息真正进行加密,必须在每个客户端都生成一对公私钥,客户端在请求时携带其公钥,服务端对返回信息使用该公钥进行加密后返回,保证只有发送该请求的客户端可以解密该信息。
但在实际操作过后,发现在客户端生成公私钥的时间过长,影响用户体验,还需从长计议。另一个问题是既然是接口加密,为什么不使用https?难道只是因为不想掏证书钱?
本文可能有学术错误或形容不恰当的地方,还请读者们多多指教!
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)