文章目录

0. 引言
前段时间公司老系统做等保三级认证,在前后端参数传输中被检测有重大漏洞,原因是原系统中使用的参数加密是对称加密,而密钥被明晃晃的存储在前端js代码中,相当于明文传输,后续结合非对称加密做了优化,今天特意整理下思路和实现方法,供大家参考。
1. 思路
1.1 对称加密的问题
首先我们来看对称加密的问题,就是加解密密钥相同,前端传输参数到后端时,就需要使用密钥加密,那密钥存储在哪里呢?
第一种就是像我们老系统那样,存储在前端js层中,当然你可以做一些算法处理来给密钥加点伪装,但实际上代码都在前端层,通过获取源码反解析也能破解,所以存储在前端肯定不可取。
第二种是通过接口实时从后端获取,这个获取接口通过token,session等身份认证控制权限,这种方式相对安全一些, 但是依然可以通过截取token来获取到密钥,所以相对安全性有限。
那么如何从根本上解决这个安全性问题呢?
那就是不能把解密密钥放到前端,但没有密钥又怎么加密呢,这就用到非对称加密了。
1.2 非对称加密的问题
非对称加密是使用一对密钥(即两个密钥)来加解密,这对密钥中包含一个公钥和一个私钥,两者相互绑定,公钥用于加密,私钥用于解密。这样在前后端加解密的过程,就可以将公钥直接放在前端,私钥存储在后端,即使被拦截到加密内容和公钥,因为没有私钥,也无法解密参数,从而解决了加密安全性问题。
截止到目前为止,好像一切都思路清晰了, 我们直接用非对称加密来加解密参数即可,但实际真的可行吗?
如果对于性能没有要求,实际上是可以满足了, 但我们做前后端参数传输一般要求越快越好,而非对称加密的性能相对对称加密性能更低,也就是加密速度更慢,特别是在参数值越大的情况下。所以考虑上性能的话,非对称的短板又出现了。
怎么解决呢?
答案是综合对称和非对称,对称的问题是密钥泄漏的安全问题,非对称的问题是加解密性能,那么我们是否可以用对称来加解密数据量更大的参数,用非对称来加密对称密钥,而这个密钥每次都随机生成,同时也随着参数一起传输到后端,这样就不用存储这个密钥了,以此实现安全与性能的平衡。 这个做法,也有一个专门的技术名称,就是“数字信封技术”
那么下面,我们就来看看如何实现它。
2. 实现
对称加密和非对称加密都有多种,这里我们使用AES作为对称加密,RSA作为非对称加密,如果需要使用其他算法的,大家可以自己拓展
2.1 java实现对称、非对称加密
1、AES对称加解密,这里我采用的AES加密模式是AES/CBC/ZeroPadding
,基于jdk1.8,核心代码如下,完整代码可在文末源码地址下载
/**
* AES加密
*
* @param content 待加密的内容
* @param encryptKey 加密密钥
* @return 加密后的byte[]
* @throws Exception
*/
private static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
Cipher cipher = Cipher.getInstance(AES_MODE);
int blockSize = cipher.getBlockSize();
byte[] dataBytes = content.getBytes(StandardCharsets.UTF_8);
// zeroPadding
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keySpec = new SecretKeySpec(encryptKey.getBytes(), MODE);
IvParameterSpec ivSpec = new IvParameterSpec(encryptKey.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(plaintext);
}
/**
* AES解密
*
* @param encryptBytes 待解密的byte[]
* @param decryptKey 解密密钥
* @return 解密后的String
* @throws Exception
*/
private static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {
Cipher cipher = Cipher.getInstance(AES_MODE);
SecretKeySpec keySpec = new SecretKeySpec(decryptKey.getBytes(), MODE);
IvParameterSpec ivSpec = new IvParameterSpec(decryptKey.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] decryptBytes = cipher.doFinal(encryptBytes);
String result = new String(decryptBytes);
if (!"".equals(result)) {
result = result.trim();
}
return result;
}
2、RSA加解密核心代码
/**
* 公钥加密
* @param data
* @param publicKeyStr
* @return
* @throws Exception
*/
public static String rsaEncrypt(String data, String publicKeyStr) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
byte[] cache;
PublicKey publicKey = getPublicKey(publicKeyStr);
Cipher cipher = Cipher.getInstance(RSA_ALG);
byte[] dataBytes = data.getBytes(CHARSET);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
int dataLen = dataBytes.length;
int afterLocal = 0;
while (dataLen - afterLocal > 0) {
if (dataLen - afterLocal > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(dataBytes, afterLocal, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(dataBytes, afterLocal, dataLen - afterLocal);
}
out.write(cache, 0, cache.length);
afterLocal = afterLocal + MAX_ENCRYPT_BLOCK;
}
byte[] rsaEncrypt = out.toByteArray();
return Base64.getEncoder().encodeToString(rsaEncrypt);
} catch (Exception e) {
e.printStackTrace();
throw new Exception("RSA 加密失败");
}finally {
out.close();
}
}
/**
* 私钥解密
* @param data
* @param privateKeyStr
* @return
* @throws Exception
*/
public static String rsaDecrypt(String data, String privateKeyStr) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
byte[] cache;
byte[] decode = Base64.getDecoder().decode(data.getBytes(CHARSET));
PrivateKey privateKey = getPrivateKey(privateKeyStr);
Cipher cipher = Cipher.getInstance(RSA_ALG);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
int dataLen = decode.length;
int afterLocal = 0;
while (dataLen - afterLocal > 0) {
if (dataLen - afterLocal > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(decode, afterLocal, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(decode, afterLocal, dataLen - afterLocal);
}
out.write(cache, 0, cache.length);
afterLocal = afterLocal + MAX_DECRYPT_BLOCK;
}
return out.toString();
} catch (Exception e) {
e.printStackTrace();
throw new Exception("RSA 解密失败");
} finally {
out.close();
}
}
3、另外还需要实现一个公私钥生成工具
public static void main(String[] args) throws Exception{
// Create a key pair generator for RSA
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// Initialize the key pair generator with a 2048-bit key size
keyPairGenerator.initialize(1024);
// Generate a key pair
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// Get the private key
PrivateKey privateKey = keyPair.getPrivate();
// Get the public key
PublicKey publicKey = keyPair.getPublic();
// Encode the keys using base64
String privateKeyEncoded = Base64.getEncoder().encodeToString(privateKey.getEncoded());
String publicKeyEncoded = Base64.getEncoder().encodeToString(publicKey.getEncoded());
// Print the keys
System.out.println("Private key:\n" + privateKeyEncoded);
System.out.println("Public key:\n" + publicKeyEncoded);
}
2.2 js实现对称、非对称加密
1、AES加解密主要基于crypto-js
来实现,这里我使用的版本是v3.1.2
,源码地址
https://gitee.com/wuhanxue/wu_study/blob/master/demo/encrypt_demo/src/main/resources/static/js/aes.js
/**
* 参数加密
* @param data
* @param keyStr
* @returns {string}
*/
function aesEncrypt(data, keyStr) {
var key = CryptoJS.enc.Latin1.parse(keyStr);
var encrypted = CryptoJS.AES.encrypt(data, key, {
iv: key,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
return encrypted.toString();
}
2、RSA加密基于jsencrypt.min.js
来实现,源码地址:
/**
* 密钥加密
* @param data
* @param publicKey
* @returns {*}
*/
function rsaEncrypt(data, publicKey) {
var rsa = new JSEncrypt();
rsa.setPublicKey(publicKey);
return rsa.encrypt(data);
}
2.3 参数加密流程实现
1、我们实现了上述过程的加解密工具包后,即可开始实现流程源码。首先要明确参数加密是在前端发起的,我们这里只演示请求参数加密,响应参数则直接明文传输即可。
/**
* 生成随机密钥
* @returns {string}
*/
function generateRandomKey() {
var length = 16;
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
调用接口前生成随机密钥,并对参数和密钥进行加密
function encrypt() {
var param = $("#param").val();
var data = {
"userName": $("#param").val()
};
// 生成密钥
var keyStr = generateRandomKey();
// 加密参数
var encryptParam = aesEncrypt(param, keyStr);
$("#aes_param").val(encryptParam);
// 加密密钥
var encryptKey = rsaEncrypt(keyStr, publicKey);//加密后的密码
$("#rsa_param").val(encryptKey);
// 发起请求
$.ajax({
url: "/user/add",
type: "POST",
headers: {
encryptKey: encryptKey
},
data: data,
processData: true,
contentType: "application/x-www-form-urlencoded", // 告诉jQuery不要去设置Content-Type请求头
success: function (res) {
console.log(res);
$("#rsa_decrypt").val(JSON.stringify(res));
},
error: function (res) {
console.error("系统错误", res)
}
});
}
2、在后端中,可以通过拦截器拦截请求,然后解密参数
通过继承HttpServletRequestWrapper
实现参数解密转换类
public class DecryptParamsWrapper extends HttpServletRequestWrapper {
private final Logger logger = LogManager.getLogger(DecryptParamsWrapper.class);
public static final String KEY_NAME = "encryptKey";
/**
* 参数
*/
private Map<String, String[]> parameterMap;
HttpServletRequest request;
public DecryptParamsWrapper(HttpServletRequest request) {
super(request);
this.request = request;
parameterMap = request.getParameterMap();
}
/**
* 获取所有参数名
*
* @return 返回所有参数名
*/
@Override
public Enumeration<String> getParameterNames() {
Vector<String> vector = new Vector<>(parameterMap.keySet());
return vector.elements();
}
/**
* 获取指定参数名的值,如果有重复的参数名,则返回第一个的值
*
* @param name 指定参数名
* @return 指定参数名的值
*/
@Override
public String getParameter(String name) {
String[] results = parameterMap.get(name);
if (results == null || results.length <= 0) {
return null;
} else if ("rows".equals(name) || "page".equals(name)) {
return results[0];
} else {
return decrypt(results[0]);
}
}
/**
* 获取指定参数名的所有值的数组
*/
@Override
public String[] getParameterValues(String name) {
String[] results = parameterMap.get(name);
if (results == null || results.length <= 0) {
return null;
// 分页参数过滤
} else if ("rows".equals(name) || "page".equals(name)) {
return results;
} else {
int length = results.length;
for (int i = 0; i < length; i++) {
String temp = decrypt(results[i]);
results[i] = temp;
}
return results;
}
}
private String decrypt(String data) {
try {
return AesUtil.aesDecrypt(data, decryptKey());
} catch (Exception e) {
logger.error("参数解密失败: ", e);
throw new RuntimeException(e);
}
}
private String decryptKey(){
String keyEncrypt = this.request.getHeader(KEY_NAME);
String key = null;
try {
key = RsaUtil.rsaDecrypt(keyEncrypt, RsaUtil.PRIVATE_KEY);
return key;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("密钥解密失败");
}
}
}
然后声明拦截器,在拦截器中调用解密转换类
@Component
@WebFilter(filterName = "decryptFilter", urlPatterns = "/*")
public class DecryptParamsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 过滤url
// String requestURI = httpRequest.getRequestURI();
// if(requestURI != null && requestURI.contains("xx/xx")){
// chain.doFilter(request, response);
// return;
// }
String keyEncrypt = httpRequest.getHeader(DecryptParamsWrapper.KEY_NAME);
if(keyEncrypt == null || keyEncrypt.length() == 0){
chain.doFilter(request, response);
return;
}
DecryptParamsWrapper wrapper = new DecryptParamsWrapper(httpRequest);
chain.doFilter(wrapper, response);
}
}
2.4 测试
这里我编写了一个测试demo, 源码见下文,增加一个后端接口,用于测试
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("add")
public R addUser(String userName, HttpServletRequest request){
// 模拟操作成功, 将解密的参数返回
try {
return R.success(userName);
} catch (Exception e) {
e.printStackTrace();
return R.fail("解密失败");
}
}
}
通过浏览器控制台可以看到调用时入参是加密的
每次的对称密钥存放在header中
后端接口的逻辑是将入参获取后再返回,可以看到已经通过拦截器自动解密了
测试通过!
3. 项目源码
https://gitee.com/wuhanxue/wu_study/blob/master/demo/encrypt_demo
4. 总结
如上我们通过对称加密实现参数值加密,对称密钥每次都随机生成,通过非对称加密实现密钥的加密,在高安全性和性能之间实现平衡。常规的前后端加解密足够使用。
如果对参数完整性还有校验要求的话,可以增加一个sign签名验证,源码中已经实现部分代码,但还未验证测试,大家可以参考实现。