java进阶:还在用明文传输前后端参数?等保3级认证不过啦?|前后端参数加解密


在这里插入图片描述

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来实现,源码地址:

https://gitee.com/wuhanxue/wu_study/blob/master/demo/encrypt_demo/src/main/resources/static/js/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签名验证,源码中已经实现部分代码,但还未验证测试,大家可以参考实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wu@55555

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值