0. 写在之前
文章写于2022年12月26日,从不查验健康码开端,到明天确定新冠肺炎改名为新冠感染,疫情铺开大约已有两周,上周项目负责人也不幸被感染,高烧居家。之前开发完接口加密后,项目的对接到目前仍被放置。
原来的项目运用的是:依据服务名、用户标识、时刻戳等信息,运用前后端(C/S)约定好的公共密钥对其进行加签生成一个sign,然后将这些信息拼接到恳求url中。此种办法只能证明该恳求是合法的,并不能对信息进行加密。
项目负责人告诉我客户想换一种接口加密的办法,所以我自然是在网络上学习了一些文章,经过自己的总结与测验,提出了这个计划。但计划还得客户检验,还没给回复,不知道他们行不行,反正我这能跑。
最近可能是有点闲了,也不知道这计划最终是否能落地,先写一篇文章记载一下吧!
1. 全体预览
- 全体思想为:
先运用AES对数据进行加密,再运用RSA对AES密钥进行加密。
- 至于为什么要运用这种办法呢?总结起来大致就是又安全速度又快。
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' // AES
import forge from 'node-forge' // RSA
import 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 = tmp
s = 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) {
// 运用公钥先验签aesKey
let 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 转base64
return 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字符串转byte
byte[] 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));
}
}
@Slf4j
public 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
@Component
public 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 get
String httpMethod = request.getMethod().toLowerCase();
// method办法
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
// method办法上面的注解
Annotation[] annotations = method.getAnnotations();
// 办法的形参参数
Object[] args = pjp.getArgs();
// 是否有@Decrypt
boolean hasDecrypt = false;
// 是否有@Encrypt
boolean 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的key
String aesKey = request.getParameter("aesKey");
log.debug("AES的加密key:[{}]", aesKey);
// 后端私钥解密的到AES的key
byte[] 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目标,并赋值给回来值o
o = 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?难道只是由于不想掏证书钱?
本文可能有学术过错或形容不恰当的地方,还请读者们多多指教!
最终当然是感谢这么好的渠道,平常都只在沸点摸鱼,今日终所以干了一件正事。