Java图片压缩/加密处理实践

文章介绍了如何在Java中结合业务场景实现图片的压缩和RSA2048签名。图片压缩采用了thumbnailator库,兼顾像素和质量,以满足特定大小需求。签名则因图片大小和性能考虑,使用RSA2048签名而非加密。同时,文章讨论了图片在网络传输中的Base64解析问题以及在高并发下处理内存管理的方法,以防止内存溢出。

Java图片压缩/加密处理实践

  • 研究某项技术或者代码框架时,如果没有清晰的业务目标,就会浅尝辄止,无法领悟其精髓;
  • 本人在回过头去看曾经的工作时,发现结合当时的业务场景,图片压缩还是有很多有意思的地方,值得跟大家分享下;

1. 业务场景

1.1 业务诉求

  • 按照上层客户的要求,需要对图片做签名,以确保传输的图片是未被篡改的真实用户头像;
  • 按照底层服务商的要求,需要把图片压缩到指定像素大小(如:640*480),且图片也必须固定大小(如:20-30KB);
  • 按照司法原则要求,我们需要保证图片是经过权威服务认证过的,不可抵赖,必要时,可以用做司法举证;

1.2 业务分析

  • 客户要求的图片防篡改校验,实际上必须保证图片源头是未被篡改的(可由我们前置的SDK抓取图片的同时,调用SDK底层混淆的so做RSA2048签名)。另外,我们也要想下,为什么是对图片做签名而不是加密?
    1. 对图片签名而不加密的原因是图片一般较大(500KB-5MB),而RSA2048加密和解密的性能极低,为了兼顾安全和性能,所以采取了RSA2048签名而不是加密,这样才能保证签名快,服务端验签也快;
    2. 有朋友可能会问为什么采用非对称加密算法RSA2048而不是对称加密算法AES256,这样加密效率就会提升很多。之所以没有采用AES256是因为秘钥安全的问题,我们可以把RSA2048的公钥放在SDK的底层so中给到客户(就算sdk被破解了也损失可控),但是AES256秘钥就只有1个,给出去了存在泄露秘钥的风险;
  • 底层服务商则是非常明确地要我们对图片做压缩:既要保证清晰度,又要文件尽量小,减少他们带宽的压力。因为他们是权威机构,也属于公共资源,提升并行服务能力意义重大;
  • 至于第3个司法举证的业务诉求,相信大部分搞研发的朋友都没有接触过。这是银行、金融、保险行业的特殊诉求:在业务处理的过程中,调用下CA供应商的服务,作为业务实际发生的凭证。此场景的做法是:仅需要把图片的摘要发给CA机构的时间戳服务做签名登记即可,以后溯源时,核对时间戳服务的签名就可以了;

综合上面的业务场景分析,核心是要做好图片验签和压缩即可。司法举证部分因为涉及专用硬件或者专用服务,无法演示,暂略。

2. 技术分析

2.1 技术预研

  • 做图片的签名比较简单,参考加解密在开源SpringBoot/SpringCloud微服务框架的最佳实践 文档,在注入SecurityFacade后,调用securityFacade.sign(base64)方法生成签名即可,签名验证也可以查看此源码;
  • 图片压缩的技术方案不算多,除了JDK原生的外,还有一个就是google出品的thumbnailator,初步调研后,决定使用thumbnailator。因为其依赖简单,API比较简洁,同时支持scale(像素大小)和quality(质量,即图片模糊度)压缩,正好能够满足诉求;
  • 图片压缩是个系统工程,需要大量图片数据去验证,同时也只能兼顾大部分场景,无法满足所有场景;
  • 图片压缩有可能压缩过头了(文件比目标大小小很多),也有可能无论怎么压缩,都无法到达指定大小;
  • 图片压缩是非常消耗内存的,需要控制好压缩次数,一旦处理不当就会直接内存溢出(OOM)了;

2.2 处理问题汇总

  • 图片在网络传输/签名验证的过程中,会偶现图片Base64无法解析的情况,原因是部分机型拍出的图片Base64带有"\n"等换行符,需要通过java.util.Base64.getMimeDecoder().decode(base64)转成二进制才可以做签名校验,注意是要先通过Base64.getMimeDecoder().decode(base64)来转换;
  • 由于图片较大,且并发较高时,在使用ParNew+CMS垃圾回收算法时,图片多次压缩会临时产生多份内存占用,且不会及时释放。通过观察JVM指标,发现是老年代内存剧增非常厉害,一旦无法分配时,JVM就直接Crash了。因此需要设置-XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=65,前一个参数是为了自动压缩老年代的内存碎片,后一个参数是调小了触发老年代FullGC的内存占用率,即当内存占用了65%后就会触发一次FullGC,通过较为频繁的FullGC来回收大文件内存,这样就不会突然导致老年代内存不够分配了。

3. 达成效果

  • 引入自研的jar包(里面内置了net.coobird:thumbnailator:0.4.17):
    <dependency>
        <groupId>com.biuqu</groupId>
        <artifactId>bq-base</artifactId>
        <version>1.0.4</version>
    </dependency>
    
  • 编写测试类ImageCompressUtilTest:
    public class ImageCompressUtilTest
    {
         
         
    
        @Test
        public void compress() throws IOException
        {
         
         
            String path = "pic/1.JPEG";
            byte[] data = FileUtil.read(path);
            Assert.assertTrue(null != data);
    
            byte[] newData = ImageCompressUtil.compress(data);
            Assert.assertTrue(null != newData);
    
            String testPath = ImageUtil.class.getResource("/").getPath();
            testPath = new File(testPath).getCanonicalPath() + "/testPic/drj-1.jpeg";
            ImageUtil.write(newData, testPath);
        }
    
        @Test
        public void compress2() throws IOException
        {
         
         
            for (int i = 1; i <= 13; i++)
            {
         
         
                String path = "pic/compress" + i + ".jpeg";
                if (i == 7)
                {
         
         
                    path = "pic/compress" + i + ".png";
                }
                byte[] data = FileUtil.read(path);
                Assert.assertTrue(null != data);
                byte[] newData = ImageCompressUtil.compress(data);
                Assert.assertTrue(null != newData);
                String testPath = ImageUtil.class.getResource("/").getPath();
                testPath = new File(testPath).getCanonicalPath() + "/testPic/test-" + i + ".jpeg";
                ImageUtil.write(newData, testPath);
            }
        }
    }
    

    考虑版本问题,把待压缩的图片和压缩后的图片打包放在文档附件了。

  • 分析其中一张图pic/compress4.jpeg的压缩效果:
    current file type by stream:PNG.
    current read stream file type:png
    current image type:png
    image[png] pixel is :{"width":1024,"height":1024,"colorType":5}.
    pixel from [1024,1024] to [640,480].
    resize image cost:95
    resize image from 1495932 to 762994.
    resize compress cost:309
    compress factor:0.55/1.0,result:762994->23407 bytes,cost:105 ms.
    compress[1495932->23407] totally cost:1474
    current file type by stream:JPEG.
    current write stream file type:jpeg
    
    • 图片原本大小为1.4M左右,经过了一轮像素resize到指定像素[640,480],图片大小也从1.4M降到700KB;
    • 再经过了一轮0.55的质量压缩,图片大小从700KB降到23KB,满足了业务诉求;

4. 编码解构

  • 核心压缩工具类ImageCompressUtil
    public final class ImageCompressUtil
    {
         
         
        /**
         * 基于图片二进制压缩(此仅为一种压缩场景,以此来理解压缩):
         * 1.先固定图片分辨率(固定为640*480,也算一种压缩)
         * 2.再压缩图片文件大小为20k-30k(主要控制文件的quality系数和scale系数,同时改变)
         *
         * @param data 图片二进制
         * @return 压缩后的图片二进制
         */
        public static byte[] compress(byte[] data)
        {
         
         
            if (null == data)
            {
         
         
                LOGGER.inf
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值