在实际业务开发过程中,会经常遇到缩略图、图片压缩、增加水印等需求,类似阿里云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,启用S3
AWS_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尺寸。

  1. 未开启加密,使用原始路径访问的方式为:(其中第一个_,可任意)
    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
  2. 未开启加密,路径使用Base64的方式,处理路由为Baes64编码:
    http://img.tuine.me/_/s:300:150:1:1/czM6Ly8xMC4wLjAuMzo5MDkwL2ltYWdlcy90ZXN0LmpwZw==
  3. 开启加密,通过秘钥和盐获取签名,如签名为:xxx,访问:
    http://img.tuine.me/xxx/s:300:150:1:1/czM6Ly8xMC4wLjAuMzo5MDkwL2ltYWdlcy90ZXN0LmpwZw==
  4. 未开启加密,获取原图片(删除处理规则即可),默认会开启压缩,压缩质量为: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;
    }
}

  1. 阿里云OSS 图片处理文档 ↩︎
  2. ImgProxy GitHub ↩︎
  3. ImgProxy options config ↩︎
  4. ImgProxy example ↩︎