在实际业务开发过程中,会经常遇到缩略图、图片压缩、增加水印等需求,类似阿里云OSS也有丰富的图片处理服务1
为提升用户体验及降低带宽压力,寻找一款可以扩展并且满足条件的方案。使用到一款开源产品:ImgProxy2, 可以通过代理的方式,完美接入S3。具体步骤如下:
安装ImgProxy
此处使用docker方式进行启动,为了方便管理使用docker-composer,配置文件如下:
version: "3"
services:
imgproxy:
image: darthsim/imgproxy:latest
ports:
- "18080:8080"
environment:
- IMGPROXY_USE_S3=true
- AWS_ACCESS_KEY_ID=
- AWS_SECRET_ACCESS_KEY=
- IMGPROXY_S3_ENDPOINT=
- IMGPROXY_ALLOWED_SOURCES=s3://
- IMGPROXY_DEVELOPMENT_ERRORS_MODE=true
- IMGPROXY_KEY=
- IMGPROXY_SALT=
- IMGPROXY_S3_REGION=us-east-1
- IMGPROXY_S3_MULTI_REGION=false
- IMGPROXY_S3_USE_DECRYPTION_CLIENT=false
- IMGPROXY_MAX_SRC_RESOLUTION=100
参数说明
IMGPROXY_USE_S3
:设置为true,启用S3AWS_ACCESS_KEY_ID
:minio账号AWS_SECRET_ACCESS_KEY
:minio密码IMGPROXY_S3_ENDPOINT
:minio访问地址,尽量配置内网地址IMGPROXY_KEY、IMGPROXY_SALT
:如果设置key和盐即开启加密访问,否则如若不配置,默认禁用加密访问。IMGPROXY_MAX_SRC_RESOLUTION
:最大分辨率,超过会报错,百万单位,此处设置1个亿。如果想快速生成可使用:echo $(xxd -g 2 -l 64 -p /dev/random | tr -d '\n')
其它参数暂不说明,详细请查看文档3
启动
docker-composer up -d
配置域名Nginx代理
server {
server_name img.tuine.me;
index index.html;
access_log /data/logs/nginx/img.tuine.me.access.log main;
error_log /data/logs/nginx/img.tuine.me.error.log info;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
location /{
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://10.0.0.2:18080;
}
listen 80;
}
基础使用说明
以上步骤顺利执行后,此时已经可以访问S3中的文件了,先大致介绍一些ImgProxy大致使用规则:
以下示例为,将图片裁剪为300*150尺寸。
- 未开启加密,使用原始路径访问的方式为:(其中第一个
_
,可任意)http://img.tuine.me/_/s:300:150:1:1/plain/http://10.0.0.3:9090/images/test.jpg
或http://img.tuine.me/_/s:300:150:1:1/plain/s3://10.0.0.3:9090/images/test.jpg
- 未开启加密,路径使用Base64的方式,处理路由为Baes64编码:
http://img.tuine.me/_/s:300:150:1:1/czM6Ly8xMC4wLjAuMzo5MDkwL2ltYWdlcy90ZXN0LmpwZw==
- 开启加密,通过秘钥和盐获取签名,如签名为:
xxx
,访问:http://img.tuine.me/xxx/s:300:150:1:1/czM6Ly8xMC4wLjAuMzo5MDkwL2ltYWdlcy90ZXN0LmpwZw==
- 未开启加密,获取原图片(删除处理规则即可),默认会开启压缩,压缩质量为:80%。
http://img.tuine.me/_/czM6Ly8xMC4wLjAuMzo5MDkwL2ltYWdlcy90ZXN0LmpwZw==
签名计算方式
salt + 处理规则 + 访问路由
使用SHA256计算HMAC摘要,然后获取Base64编码。
Java实现
github有大部分流行语言的签名用例,可查看4
基础配置
img-proxy:
key:
salt:
url: http://img.tuine.me
Controller举例
@RestController
@Slf4j
@RequestMapping("img")
public class ImgProxyController {
@Autowired
private ImgProxyService imgProxyService;
/**
* 获取指定尺寸图片
*
* @return /
*/
@GetMapping(value = "size")
public Result size(@RequestParam("url") String url,
@RequestParam("width") Integer width,
@RequestParam("height") Integer height) {
String processingOptions = "s:" + width + ":" + height + ":1:1";
String imgProxyUrl = imgProxyService.getImgProxyUrl(url, processingOptions);
return ResultGenerator.success(Map.of("url", imgProxyUrl));
}
/**
* 获取压缩后的图片
*
* @return /
*/
@GetMapping(value = "compOriginal")
public Result compressionOriginal(@RequestParam("url") String url) {
String imgProxyUrl = imgProxyService.getImgProxyUrl(url, null);
return ResultGenerator.success(Map.of("url", imgProxyUrl));
}
}
Service基础操作:
@Service
@Slf4j
public class ImgProxyService {
@Value("${img-proxy.key}")
private String key;
@Value("${img-proxy.salt}")
private String salt;
@Value("${img-proxy.url}")
private String proxyUrl;
@Value("${minio.bucket-name}")
private String minioBucketName;
/**
* 根据路径和规则过去签名地址
*
* @param path 路径
* @param processingOptions 处理规则
*/
public String getImgProxyUrl(String path, String processingOptions) {
byte[] readKey = hexStringToByteArray(key);
byte[] readSalt = hexStringToByteArray(salt);
String separator = "/";
// 图像的原始 URL s3://tuine/tmp/test.jpg
if (path.startsWith("/")) {
path = path.substring(1);
}
String base64Url = cn.hutool.core.codec.Base64.encode("s3://" + minioBucketName + separator + path);
// 构建签名字符串
String newPath = (StringUtils.isBlank(processingOptions) ? "" : separator + processingOptions) + separator + base64Url;
// 最终访问路径开始带/
String pathWithHash = "";
try {
pathWithHash = signPath(readKey, readSalt, newPath);
} catch (Exception e) {
log.error("图片转换签名失败。", e);
throw new BadRequestException("处理图片失败");
}
return proxyUrl + pathWithHash;
}
public static String signPath(byte[] key, byte[] salt, String path) throws Exception {
final String HMACSHA256 = "HmacSHA256";
Mac sha256HMAC = Mac.getInstance(HMACSHA256);
SecretKeySpec secretKey = new SecretKeySpec(key, HMACSHA256);
sha256HMAC.init(secretKey);
sha256HMAC.update(salt);
String hash = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256HMAC.doFinal(path.getBytes()));
return "/" + hash + path;
}
private static byte[] hexStringToByteArray(String hex) {
if (hex.length() % 2 != 0) {
throw new IllegalArgumentException("Even-length string required");
}
byte[] res = new byte[hex.length() / 2];
for (int i = 0; i < res.length; i++) {
res[i] = (byte) ((Character.digit(hex.charAt(i * 2), 16) << 4) | (Character.digit(hex.charAt(i * 2 + 1), 16)));
}
return res;
}
}