个人项目:交际支付项目(小老板)

作者:三哥,j3code.cn

文档体系:admire.j3code.cn/note

交际支付类的项目,怎么能没有图片上传功用呢!

涉及到文件存储我第一时刻就想到了 OSS 目标存储服务(腾讯叫 COS),可是接着我又想到了”OSS 被刷 150 T 的流量,1.5 W 瞬间就没了?“。

本来想着是自己建立一套 MinIO ,但后来一想服务器的开销又要大了,仍是作算了。就在此刻,我脑袋突然灵光了一下,已然目标存储的流量是因为资源 url 走漏导致的外界不断的拜访 url 使公网流量剧增然后引起巨额消费,那我能不能不走漏这个 url 呢!

理论上是能够不直接给用户云存储的 url ,那用户如何拜访资源?

转化,当用户上传图片时,将云存储的 url 保存入库,而回来用户一个本体系的资源拜访接口。当用户拜访该接口时,体系从库中获取实在 url 进行资源拜访,并回来资源给用户,完成一次转化。

尽管能够解决 url 走漏问题,可是也是有功用消耗(从直接拜访,变为直接拜访,并且体系挂了,资源就不行用)。

方案,尽管弯曲了点,但为了 money ,献身一点是值得的(后来考虑了一下,觉得仍是有些问题,文章最后会说)。并且即便有人经过刷体系的接口拜访资源,也没事,体系有很强的限流和黑名单处理,不会产生过多的公网流量费用的。

那下面咱们就先开通相关功用,然后再编码完成。

1、腾讯云目标存储创立

地址:console.cloud.tencent.com/cos

开通目标存储的过程仍是非常简单的,具体过程如下:

1)开通功用

看我的奇思妙想,解决对象存储被刷钱的情况

2)装备存储桶

看我的奇思妙想,解决对象存储被刷钱的情况

下一步

看我的奇思妙想,解决对象存储被刷钱的情况

下一步

看我的奇思妙想,解决对象存储被刷钱的情况

3)创立拜访的密钥

腾讯的所有 API 接口都需求这个拜访密钥,假如曾经创立过就能够直接拿来运用

看我的奇思妙想,解决对象存储被刷钱的情况

下一步

看我的奇思妙想,解决对象存储被刷钱的情况

根本的功用咱们现已开通了,并且以后咱们只需向这个存储桶中上传图片即可。

2、SpringBoot 对接目标存储

已然准备工作都现已完成了,那就开始编写上传文件的代码吧!当然,这儿咱们仍是要凭借官方文档,便于咱们开发,地址如下:

cloud.tencent.com/document/pr…

2.1 装备准备

先来考虑一下,关于腾讯 COS 文件上传需求那些装备:

  1. 云 API 的 SecretId 和 SecretKey
  2. 桶称号
  3. 文件上传巨细约束
  4. 再加一个 cos 上传后的拜访域名

ok,大致就这些,那咱们就先来写个装备文件:application-cos.yml

tx.cos:
  # 云 API 的 SecretId
  secret-id: ENC(X7Uu6Y0QD6aCeUmNhyqv1jcr8fSN+fqM/FSP/rqhM+6pkbte2LW5gR3wntsm24n3NAg6sIwBC3pqm1lSNWwElc3iuGe3lE4L/k3zih+EstM=)
  # 云 API 的 SecretKey
  secret-key: ENC(ui3jqYJpyTRtPAizYdtll2Zc1EVzUjK28vjTyD+t3AIydQO6I+JQOVacc5+NJVybsbFptELswKhY55OQLW+BKfujNTOYEM/zb4CMi+AK80w=)
  # 域名拜访
  domain: ENC(oRsaRjwRCVLEYfcNB0CjPGyqSMxGM5uzWnSpSifauLF7c5YMt5hZFi7xAthJI4CjmOLVA810Jbgy8lnkKrXUH0g1ee14cr67xSdtPRy1ZaJOXQOMlBgCKNO2wDBg2YW2)
  # 文件上传的桶称号
  bucket-name: ENC(TUsQfDEFx6KSAOpRwG7UYOJbGwnFT0Z9tjS4h+/HeenAE3XbhKsCwn3TTo80n5tUUP9Dzrnu+Ck84FNSYQk5fw==)
spring:
  servlet:
    multipart:
      # 约束文件上传巨细
      max-request-size: 5MB
      max-file-size: 5MB

注:这儿,我的装备值是加密的,所以你们需求装备自己的值

再依据这个装备文件,写一个对应的装备类:

地址:cn.j3code.common.config

@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "tx.cos")
public class TxCosConfig {
    /**
     * 拜访域名
     */
    private String domain;
    /**
     * 桶称号
     */
    private String bucketName;
    /**
     * api密钥中的secretId
     */
    private String secretId;
    /**
     * api密钥中的使用密钥
     */
    private String secretKey;
}

2.2 上传文件代码

这儿,咱们先完成单个文件的上传,那来考虑一下,上传文件应该需求那些过程:

  1. 校验文件称号
  2. 重新生成一个新文件称号
  3. 腾讯 COS 文件存储途径生成
  4. 文件上传
  5. 拼接文件拜访 url

对应此过程的流程图,如下:

看我的奇思妙想,解决对象存储被刷钱的情况

1)controller 编写

方位:cn.j3code.other.api.v1.controller

@Slf4j
@AllArgsConstructor
@ResponseResult
@RestController
@RequestMapping(UrlPrefixConstants.WEB_V1 + "/image/upload")
public class ImageUploadController {
    private final FileService fileService;
    /**
     * 图片上传
     * @param file 文件
     * @return 回来文件 url
     */
    @PostMapping("")
    public String upload(@RequestParam("file") MultipartFile file){
        return fileService.imageUpload(file);
    }
}

2)service 编写

方位:cn.j3code.other.service

public interface FileService {
    String imageUpload(MultipartFile file);
}
@Slf4j
@AllArgsConstructor
@Service
public class FileServiceImpl implements FileService {
    /**
     * 允许上传的图片类型
     */
    public static final Set<String> IMG_TYPE = Set.of("jpg", "jpeg", "png", "gif");
    /**
     * 腾讯 cos 装备
     */
    private final TxCosConfig txCosConfig;
    private final UrlKeyService urlKeyService;
    /**
     * 图片上传
     *
     * @param file
     * @return
     */
    @Override
    public String imageUpload(MultipartFile file) {
        // 文件称号
        String newFileName = getNewFileName(file);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String format = formatter.format(LocalDate.now());
        // key = /用户id/年月日/文件
        String key = SecurityUtil.getUserId() + "/" + format + "/" + newFileName;
        String prefix = newFileName.substring(0, newFileName.lastIndexOf(".") - 1);
        String suffix = newFileName.substring(newFileName.lastIndexOf(".") + 1);
        File tempFile = null;
        File rename = null;
        try {
            // 生成临时文件
            tempFile = File.createTempFile(prefix, "." + suffix);
            file.transferTo(tempFile);
            // 重命名文件
            rename = FileUtil.rename(tempFile, newFileName, true, true);
            // 上传
            upload(new FileInputStream(rename), key);
        } catch (Exception e) {
            log.error("imageUpload-error:", e);
        } finally {
            if (Objects.nonNull(tempFile)) {
                FileUtil.del(tempFile);
            }
            if (Objects.nonNull(rename)) {
                FileUtil.del(rename);
            }
        }
        // 回来拜访链接
        return initUrl(key);
    }
    /**
     * 初始化图片文件拜访 url(本地url和第三方url)
     *
     * @param key 途径
     * @return
     */
    private String initUrl(String key) {
        // 拼装第三方 url
        String imageUrl = txCosConfig.getDomain() + "/" + key;
        // 保存 url 到 数据库
        UrlKey urlKey = new UrlKey()
            .setUrl(imageUrl)
            .setKey(RandomUtil.randomString(16) + RandomUtil.randomString(16) + RandomUtil.randomString(16))
            .setUserId(SecurityUtil.getUserId());
        // 保存成功,回来本地中转的 url 出去
        boolean save = Boolean.FALSE;
        try {
            save = urlKeyService.save(urlKey);
        } catch (Exception e) {
        }
        if (save) {
            return CallbackUrlConstants.IMAGE_OPEN_URL + urlKey.getKey();
        }
        // 保存失败,直接把第三方 url 回来给用户
        return imageUrl;
    }
    /**
     * 文件上传到第三方
     *
     * @param fileStream 文件流
     * @param path       途径
     */
    private void upload(InputStream fileStream, String path) {
        PutObjectResult putObjectResult = COSClientUtil.getCosClient(txCosConfig)
            .putObject(new PutObjectRequest(txCosConfig.getBucketName(), path, fileStream, null));
        log.info("upload-result:{}", JSON.toJSONString(putObjectResult));
    }
    /**
     * 生成一个新文件称号
     * 会校验文件称号和类型
     *
     * @param file 文件
     * @return
     */
    private String getNewFileName(MultipartFile file) {
        String originalFilename = file.getOriginalFilename();
        if (StringUtil.isEmpty(originalFilename)) {
            throw new SysException("文件称号获取失败!");
        }
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        if (!IMG_TYPE.contains(suffix.substring(1))) {
            throw new SysException(String.format("仅允许上传这些类型图片:%s", JSON.toJSONString(IMG_TYPE)));
        }
        return RandomUtil.randomString(8) + SnowFlakeUtil.getId() + suffix;
    }
}

代码写的很详细了,应该能看懂,但,有两点我没有提,便是:COSClientUtil 和 UrlKeyService,下面就来结介绍。

2.2.1 cos 客户端装备提取

体系中肯定有许多的文件上传,难道是每上传一次,就装备一次 cos 客户端吗?显然不是,这个 cos 客户端肯定是要抽出来的,大局体系中咱们只装备一次。也即只要第一次过来是创立 cos 客户端,后续过来的文件上传请求直接回来创立好的 cos 客户端就行。

COSClientUtil 类便是我抽的公共 cos 客户获取类,具体完成如下:

方位:cn.j3code.other.util

public class COSClientUtil {
    /**
     * 统一 cos 上传客户端
     */
    private static COSClient cosClient;
    public static COSClient getCosClient(TxCosConfig txCosConfig) {
        if (Objects.isNull(cosClient)) {
            synchronized (COSClient.class) {
                if (Objects.isNull(cosClient)) {
                    // 1 初始化身份
                    COSCredentials cred = new BasicCOSCredentials(txCosConfig.getSecretId(), txCosConfig.getSecretKey());
                    // 2 创立装备,及设置地域
                    ClientConfig clientConfig = new ClientConfig(new Region("ap-guangzhou"));
                    // 3 生成 cos 客户端。
                    cosClient = new COSClient(cred, clientConfig);
                }
            }
        }
        return cosClient;
    }
}

私有构造器,且之对外提供 getCosClient 方法获取 COSClient 目标,保证大局只要一个 cos 客户端装备。

2.2.2 隐藏云存储 URL 处理

还记得 FileServiceImpl 类中有个 UrlKeyService 属性嘛,这个类便是做 云存储 URL 隐藏及中转功用的。

具体做法如图:

看我的奇思妙想,解决对象存储被刷钱的情况

文件上传部分咱们现已写好了,不过有点超前的意思了,不过不要紧,看整体就行。

从上面咱们要开始捉住一个细节了,便是映射联系,即 key 和 url 的映射。这儿我用的是 MySQL 保存,也即用表来存,并没有用 Redis。这儿我的考虑是,后续能够把表中的数据守时刷到 Redis 中,接着拜访的顺序是从 Redis 中找映射,没有再去 MySQL 中找。

不过,咱们首要仍是把数据先存表再说,先来看看映射表结构字段:

id

user_id

key

url

create_time

update_time

ok,就这些字段,把用户 id 加上是为了好回溯看看是谁上传了图片。

SQL 如下:

CREATE TABLE `sb_url_key` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `key` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'key',
  `url` varchar(200) COLLATE utf8_unicode_ci NOT NULL COMMENT '资源url',
  `user_id` bigint(20) DEFAULT NULL COMMENT '上传用户',
  `create_time` datetime DEFAULT NULL COMMENT '创立时刻',
  `update_time` datetime DEFAULT NULL COMMENT '修改时刻',
  PRIMARY KEY (`id`),
  KEY `key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

紧接着便是经过 MyBatisX 插件生成对应的实体、service、mapper 代码了,不过多赘述。那,现在就来开发用户拜访图片资源,咱们如何去请求第三方,然后回来用户图片 byte[] 资源数组吧!

1)controller 编写

方位:cn.j3code.other.api.v1.controller

@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.OPEN + "/resource/image")
public class ImageResourceController {
    private final UrlKeyService urlKeyService;
    /**
     * 获取图片 base64
     *
     * @param key
     * @return
     * @throws Exception
     */
    @GetMapping("/base64/{key}")
    public String imageBase64(@PathVariable("key") String key) throws Exception {
        UrlKey urlKey = urlKeyService.oneByKey(key);
        return "data:image/jpg;base64," + Base64Encoder.encode(IoUtil.readBytes(new URL(urlKey.getUrl()).openStream()));
    }
    /**
     * 获取图片 byte 数组
     *
     * @param key
     * @return
     * @throws Exception
     */
    @GetMapping(value = "/io/{key}", produces = MediaType.IMAGE_JPEG_VALUE)
    public byte[] imageIo(@PathVariable("key") String key) throws Exception {
        UrlKey urlKey = urlKeyService.oneByKey(key);
        return IoUtil.readBytes(new URL(urlKey.getUrl()).openStream());
    }
}

注意:这儿写了两个方法,目的是回来两种不同方法的图片资源:base64 和 byte[]。且,这种资源拜访的接口,咱们体系的相关拦截器请放行,如:认证,ip 记录等拦截器。

2)service 编写

方位:cn.j3code.other.service

public interface UrlKeyService extends IService<UrlKey> {
    UrlKey oneByKey(String key);
}
@Service
public class UrlKeyServiceImpl extends ServiceImpl<UrlKeyMapper, UrlKey>
    implements UrlKeyService {
    @Override
    public UrlKey oneByKey(String key) {
        UrlKey urlKey = lambdaQuery().eq(UrlKey::getKey, key).one();
        if (Objects.isNull(urlKey)) {
            throw new SysException(SysExceptionEnum.NOT_DATA_ERROR);
        }
        return urlKey;
    }
}

ok,这样咱们就处理好了,可是细心想想这种中转的方法有什么问题。

2.3 考虑

2.2 节咱们现已完成了文件上传和防止 cos 拜访 url 走漏的操作,可是我留了个问题,便是考虑这种方法有什么问题。

下面是我的考虑:

  1. 用户上传的图片,拜访时每次都会经过本体系,造成了本体系的压力
  2. 假如一个页面需求回显的图片过多,那页面响应会不会很慢
  3. 假如体系溃散了或许服务溃散了,会导致图片不行拜访,但其实第三方 url 是没有问题的

好吧,其实上面总结就两个问题,即:功用可用性

这儿的解决方法是,假如资金充裕并且 COS 做了是非名单等之类的防御措施能够直接把 COS 的原始 url 回来出去,没必要把图片资源压力给我咱们本体系。假如你不是这种情况,那么就给图片拜访接口添加布置资源,即升级服务器添加内存和借款,提高资源拜访效率及体系功用。

以上便是本节内容,假如文章的中转方法有啥缺乏或许您有什么意见,欢迎一同讨论研究。