金融场景下Java微服务图片压缩/加密等处理实战

news/2024/10/17 6:27:51/

目录导读

  • 金融场景下Java微服务图片压缩/加密等处理实战
    • 1. 业务场景
      • 1.1 业务诉求
      • 1.2 业务分析
    • 2. 技术分析
      • 2.1 技术预研
      • 2.2 处理问题汇总
    • 3. 达成效果
    • 4. 编码解构

金融场景下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
    {@Testpublic 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);}@Testpublic 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.info("invalid compress image data.");return null;}ImageFactor factor = new ImageFactor(data.length);return compress(data, factor);}/*** 基于图片大小压缩** @param data   图片二进制* @param factor 压缩因子* @return 压缩后的图片二进制*/public static byte[] compress(byte[] data, ImageFactor factor){if (null == data){LOGGER.info("invalid compress image data.");return null;}long start = System.currentTimeMillis();int size = data.length;int newSize = size;try{ImagePixel pixel = ImageUtil.getImagePixel(data);if (null == pixel){LOGGER.info("no compress parameter.");return data;}byte[] newData = mixCompress(data, pixel, factor);if (null != newData && newData.length > factor.getMaxSize()){ImageFactor nextFactor = factor.next(newData.length);byte[] multiData = multiFactorCompress(newData, nextFactor);newData = getBestData(newData, multiData);}if (null != newData){newSize = newData.length;}return newData;}finally{LOGGER.info("compress[{}->{}] totally cost:{}", size, newSize, (System.currentTimeMillis() - start));}}/*** 多因子压缩* 基于图片文件大小去压缩图片的质量和图片的像素大小** @param data   图片二进制* @param factor 图片压缩因子* @return 压缩后的图片二进制*/public static byte[] multiFactorCompress(byte[] data, ImageFactor factor){while (factor.canCompress()){byte[] newData = factorCompress(data, factor);if (null == newData){break;}data = newData;ImageFactor next = factor.next(newData.length);if (factor == next){break;}factor = next;}return data;}/*** 多因子压缩* 基于图片文件大小去压缩图片的质量和图片的像素大小** @param data   图片二进制* @param factor 图片压缩因子* @return 压缩后的图片二进制*/public static byte[] factorCompress(byte[] data, ImageFactor factor){long start = System.currentTimeMillis();if (null == data){return null;}int size = data.length;int newSize = size;float quality = factor.toQualityRate();float scale = factor.toScaleRate();InputStream in = new ByteArrayInputStream(data);ByteArrayOutputStream out = new ByteArrayOutputStream();Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(in).outputFormat(FileType.JPEG.name());try{builder.outputQuality(quality).scale(scale).toOutputStream(out);byte[] newData = out.toByteArray();newSize = newData.length;return newData;}catch (IOException e){LOGGER.error("failed to compress by quality or scale.", e);}finally{IOUtils.closeQuietly(out);IOUtils.closeQuietly(in);long cost = System.currentTimeMillis() - start;LOGGER.info("compress factor:{}/{},result:{}->{} bytes,cost:{} ms.", quality, scale, size, newSize, cost);}return null;}/*** 基于像素(宽和高)去压缩图片(存在等比例拉升/缩窄的可能)** @param data  图片二进制对象* @param pixel 图片新的像素* @return 压缩后的新图片二进制对象*/public static byte[] pixelCompress(byte[] data, ImagePixel pixel){return pixelCompress(data, pixel, BufferedImage.TYPE_INT_RGB);}/*** 基于像素(宽和高)去压缩图片** @param data     图片二进制对象* @param pixel    图片新的像素* @param colorTye 色彩类型* @return 压缩后的新图片二进制对象*/public static byte[] pixelCompress(byte[] data, ImagePixel pixel, int colorTye){if (null == data || null == pixel){LOGGER.error("invalid pixel compress parameter.");return null;}long start = System.currentTimeMillis();InputStream in = null;try{in = new ByteArrayInputStream(data);BufferedImage image = ImageIO.read(in);int width = image.getWidth();int height = image.getHeight();LOGGER.info("pixel from [{},{}] to [{},{}].", width, height, pixel.getWidth(), pixel.getHeight());BufferedImage newImage = resize(image, pixel, colorTye);byte[] newData = ImageUtil.toBytes(newImage);LOGGER.info("resize image from {} to {}.", data.length, newData.length);return newData;}catch (IOException e){LOGGER.error("failed to resize image.", e);}finally{IOUtils.closeQuietly(in);LOGGER.info("resize compress cost:{}", System.currentTimeMillis() - start);}return null;}/*** 重置图片的像素** @param image 图片对象* @param pixel 图片的像素* @param type  图片对象指定的色彩类型,比如BufferedImage.TYPE_INT_RGB表示基于RGB三原色* @return 新的图片对象*/public static BufferedImage resize(BufferedImage image, ImagePixel pixel, int type){long start = System.currentTimeMillis();BufferedImage newImage = new BufferedImageBuilder(pixel.getWidth(), pixel.getHeight(), type).build();Resizers.PROGRESSIVE.resize(image, newImage);LOGGER.info("resize image cost:{}", (System.currentTimeMillis() - start));return newImage;}/*** 综合使用图片像素和质量系数各压缩1次* <p>* 1.优先使用标准的RGB3原色压缩一轮图片像素大小,如果大小大于限定大小,则做一轮质量压缩(质量系数0.55)[个人经验做法]* 2.如果质量压缩后,图片急剧变小,则重新使用原图片的色彩类型重新按照上述第1条再次压缩一遍;* 3.选取最佳的图片大小二进制:压缩后如果还需要继续压缩,则选取压缩后的图片二进制,否则选择压缩前的图片二进制;** @param data   图片二进制* @param pixel  图片原始像素* @param factor 图片压缩多因子参数* @return 综合压缩后的新图片二进制*/private static byte[] mixCompress(byte[] data, ImagePixel pixel, ImageFactor factor){//1.先基于像素大小压缩一轮(使用标准的色彩类型,有可能导致图片严重失真)byte[] pixelData1 = pixelCompress(data, pixel.compress());//图片的限定大小int maxSize = factor.getMaxSize();if (needCompress(pixelData1, maxSize)){//2.再基于图片的大小压缩一轮图片质量和scale(scale对应的就是图片像素大小系数)byte[] factorData1 = factorCompress(pixelData1, factor);if (null != factorData1 && factorData1.length < maxSize){//图片文件大小急剧变小(图片严重失真)if (!ImageFactor.validSize(pixelData1.length, factorData1.length)){//3.如果第一轮像素大小压缩后导致图片文件大小急剧变小(图片严重失真),重新基于原图的色彩类型再来压缩一次byte[] pixelData2 = pixelCompress(data, pixel.compress(), pixel.getColorType());if (needCompress(pixelData2, maxSize)){byte[] factorData2 = factorCompress(pixelData2, factor);return getBestData(pixelData2, factorData2);}return getBestData(data, pixelData2);}}return getBestData(pixelData1, factorData1);}return getBestData(data, pixelData1);}/*** 是否需要压缩** @param data 压缩后的图片二进制* @param size 图片文件最大的大小限制* @return true表示需要压缩*/private static boolean needCompress(byte[] data, int size){return null != data && data.length > size;}/*** 获取最佳图片数组大小** @param data    压缩前的二进制* @param newData 压缩后的二进制* @return 最佳的二进制*/private static byte[] getBestData(byte[] data, byte[] newData){if (null != newData){return newData;}return data;}private ImageCompressUtil(){}/*** 日志句柄*/private static final Logger LOGGER = LoggerFactory.getLogger(ImageCompressUtil.class);
    }
    

    代码注释已经比较详尽了,我再补充下压缩逻辑设计:

    1. 先对图片做像素大小resize(使用三原色)压缩处理,即直接把图片像素变成640*480,这样图片文件大小一般会压缩比较多;
    2. 再对图片做一次多因子(主要是图片质量)压缩;
    3. 如果第2步的多因子压缩相比第1步中的像素压缩大小变化太大,就重新做一轮像素resize压缩(使用原颜色体系),接着重新做一轮多因子压缩;
    4. resize和多因子混合压缩完毕后,就进入多因子循环压缩阶段,直到图片文件大小满足要求为止;
    5. 在第4步循环压缩的过程中,如果连续压缩多次(目前定的阈值是2次)都还是比预期最大的文件大小还要大,则多因子压缩除了考虑质量因素外,还需要对像素按照百分比进行压缩;
    6. 多因子压缩时,质量系数每次均取极值和当前值的平均值。
  • 多因子压缩系数计算的ImageFactor 代码如下:
    @Data
    public class ImageFactor
    {public ImageFactor(int size){this.size = size;}/*** 是否是合法的大小** @param size    压缩前的大小* @param newSize 新图片文件大小* @return true表示合理压缩, false表示压缩过度*/public static boolean validSize(int size, int newSize){return MIN_SIZE_RATE < MathUtil.toRate(newSize, size);}/*** 是否能压缩** @return true表示能压缩*/public boolean canCompress(){return this.leftCompressTimes > 0 && this.size <= this.maxSize && this.size >= this.minSize;}/*** 计算下一个质量因子** @param newSize 压缩后的大小* @return 下一次压缩的质量因子*/public ImageFactor next(int newSize){ImageFactor factor = new ImageFactor(this.size);BeanUtils.copyProperties(this, factor);this.leftCompressTimes--;if (newSize > this.maxSize){this.beyondTimes++;if (this.beyondTimes >= this.timesThreshold){int scale = MathUtil.avg(this.maxSize, this.size) * MAX_Q / this.size;if (this.scale < MAX_Q){scale *= MathUtil.avg(this.scale, MAX_Q);}factor.setScale(scale);factor.setQuality(DEFAULT_Q);factor.setMinQuality(MIN_Q);factor.setMaxQuality(MAX_Q);}else{factor.setQuality(MathUtil.avg(this.maxQuality, this.quality));factor.setMaxQuality(this.quality);}}else if (newSize < this.minSize){this.beyondTimes = 0;factor.setQuality(MathUtil.avg(this.maxQuality, this.quality));factor.setMinQuality(this.quality);}else{this.beyondTimes = 0;return this;}factor.setLeftCompressTimes(this.leftCompressTimes);factor.setSize(newSize);factor.setBeyondTimes(this.beyondTimes);return factor;}/*** scale系数(百分比)** @return 大小系数*/public float toScaleRate(){return MathUtil.toRate(this.scale);}/*** 质量系数(百分比)** @return 质量系数*/public float toQualityRate(){return MathUtil.toRate(this.quality);}/*** 大小系数,大小在(0,100)之间*/private int scale = MAX_Q;/*** 质量系数,大小在(0,100)之间*/private int quality = DEFAULT_Q;/*** 最小质量系数*/private int minQuality = MIN_Q;/*** 最大质量系数*/private int maxQuality = MAX_Q;/*** 连续过大或者过小的持续次数*/private int beyondTimes;/*** 最大压缩次数*/private int maxCompressTimes = 5;/*** 剩余压缩次数*/private int leftCompressTimes = maxCompressTimes;/*** 连续过大或者过小的持续次数*/private int timesThreshold = Const.TWO;/*** 图片文件大小*/private int size;/*** 最小图片大小*/private int minSize = 20 * 1024;/*** 最大图片大小*/private int maxSize = 30 * 1024;/*** 最大质量系数*/private static final int MAX_Q = 100;/*** 最大质量系数(默认值)*/private static final int DEFAULT_Q = 55;/*** 最小质量系数(默认值)*/private static final int MIN_Q = 30;/*** 最小的图片大小压缩率*/private static final float MIN_SIZE_RATE = 0.01f;
    }
    

http://www.ppmy.cn/news/589375.html

相关文章

【Python爬虫与数据分析】进阶语法

目录 一、异常捕获 二、迭代器 三、拆包、聚合、映射 四、filter() 函数 五、匿名函数 六、闭包 七、装饰器 一、异常捕获 异常捕获可增强程序的健壮性&#xff0c;即程序在遇到遇到异常的时候并不会做中断处理&#xff0c;而是会将异常抛出&#xff0c;由程序员来分析…

红警3修改器无法连接服务器,红警3序列号修改器-不能加入游戏怎么办?红警3连局域网说cd-– 手机爱问...

2018-03-05 为什么我的红警不能联局域网 红警局域网联机的具体方法: 适用于原版红警、尤里复仇&#xff0c;及任何同样的扩展版。 第一步&#xff1a;安装IPX协议。 方法&#xff1a; 控制面板——网络连接(或网上邻居属性)——本地连接属性 ——在“此连接使用下列项目”中&am…

Eclipse 3.3 汉化包下载

Eclipse 是一款很好的IDE环境&#xff0c;功能完整而成熟。它使用 Java 语言开发&#xff0c;而且属于开源项目&#xff0c;网上充足的插件&#xff0c;保证了其强大的可扩展性。 Eclipse 的语言包也是以插件的形式来提供的。很可惜的是&#xff0c;从3.3版本开始&#xff0c;…

红警资源系列一 红警资源导出

XCC Mixer 1.46 解包mix文件&#xff0c;红警中比较重要的是ra2.mix&#xff0c;基本红警所有的资源都在这个包中。 对ra2.mix解包 双击可查看mix的包内容。 里面文件基本有以下两类 .shp 存储帧动画&#xff0c;比方说动员兵的每一个动作都在这个文件中&#xff0c;还有场景…

いもけんぴ 三作 汉化补丁

这几个程序 我已经完全逆向出全部源代码 所有汉化补丁 都在VS2010 下编译通过 能完全逆向出源代码 并修改成为自己的才叫真正的破解..... 支持 OS:Windows XP/VISTA/7 其中 Windows XP 需要安装 .net Framework 2.0 或者3.0系列 显卡需支持OpenGL 非简体中文系统注意&#xff…

红警2联机终极补丁

红警2联机终极补丁 转载于:https://blog.51cto.com/amcto111/498711

红警3破解版有感

红警3破解版有点类似定制版的红警3破解版(当然它的主题一定是Black 系的)。红警3破解版预装了一些便捷的插件,可以让您直达某些红警3破解版(美国黑人社区)。最搞笑的是他们还制作了一个红警3破解版(基于Google 的定制搜索)据说只能搜到红警3破解版(黑人站点。) 昨天下了红…

PL SQL Developer 中文汉化补丁-亲测最新版本

PLSQL Developer汉化补丁下载地址&#xff1a; https://download.csdn.net/download/rxtanlian/11830634 汉化补丁&#xff1a; 百度云盘链接: https://pan.baidu.com/s/1gfeXLnH 密码: 3583 一、双击运行补丁 二、选择你PLSQL Developer 的安装目录 看图 三、点击 蓝色三角…