- 对图片签名而不加密的原因是图片一般较大(500KB-5MB),而RSA2048加密和解密的性能极低,为了兼顾安全和性能,所以采取了RSA2048签名而不是加密,这样才能保证签名快,服务端验签也快;
- 有朋友可能会问为什么采用非对称加密算法RSA2048而不是对称加密算法AES256,这样加密效率就会提升很多。之所以没有采用AES256是因为秘钥安全的问题,我们可以把RSA2048的公钥放在SDK的底层so中给到客户(就算sdk被破解了也损失可控),但是AES256秘钥就只有1个,给出去了存在泄露秘钥的风险;
综合上面的业务场景分析,核心是要做好图片验签和压缩即可。司法举证部分因为涉及专用硬件或者专用服务,无法演示,暂略。
SecurityFacade
后,调用securityFacade.sign(base64)
方法生成签名即可,签名验证也可以查看此源码;java.util.Base64.getMimeDecoder().decode(base64)
转成二进制才可以做签名校验,注意是要先通过Base64.getMimeDecoder().decode(base64)来转换;ParNew+CMS
垃圾回收算法时,图片多次压缩会临时产生多份内存占用,且不会及时释放。通过观察JVM指标,发现是老年代内存剧增非常厉害,一旦无法分配时,JVM就直接Crash了。因此需要设置-XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=65
,前一个参数是为了自动压缩老年代的内存碎片,后一个参数是调小了触发老年代FullGC的内存占用率,即当内存占用了65%后就会触发一次FullGC,通过较为频繁的FullGC来回收大文件内存,这样就不会突然导致老年代内存不够分配了。net.coobird:thumbnailator:0.4.17
):<dependency>
<groupId>com.biuqugroupId>
<artifactId>bq-baseartifactId>
<version>1.0.4version>
dependency>
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,满足了业务诉求;
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次
*
* 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);
}
代码注释已经比较详尽了,我再补充下压缩逻辑设计:
- 先对图片做像素大小resize(使用三原色)压缩处理,即直接把图片像素变成640*480,这样图片文件大小一般会压缩比较多;
- 再对图片做一次多因子(主要是图片质量)压缩;
- 如果第2步的多因子压缩相比第1步中的像素压缩大小变化太大,就重新做一轮像素resize压缩(使用原颜色体系),接着重新做一轮多因子压缩;
- resize和多因子混合压缩完毕后,就进入多因子循环压缩阶段,直到图片文件大小满足要求为止;
- 在第4步循环压缩的过程中,如果连续压缩多次(目前定的阈值是2次)都还是比预期最大的文件大小还要大,则多因子压缩除了考虑质量因素外,还需要对像素按照百分比进行压缩;
- 多因子压缩时,质量系数每次均取极值和当前值的平均值。
@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;
}