社交支付類的項目,怎麼能沒有圖檔上傳功能呢!
涉及到檔案存儲我第一時間就想到了 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 檔案上傳需要那些配置:
- 雲 API 的 SecretId 和 SecretKey
- 桶名稱
- 檔案上傳大小限制
- 再加一個 cos 上傳後的通路域名
ok,大緻就這些,那咱們就先來寫個配置檔案:application-cos.yml
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
java複制代碼@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 上傳檔案代碼
這裡,我們先實作單個檔案的上傳,那來思考一下,上傳檔案應該需要那些步驟:
- 校驗檔案名稱
- 重新生成一個新檔案名稱
- 騰訊 COS 檔案存儲路徑生成
- 檔案上傳
- 拼接檔案通路 url
對應此步驟的流程圖,如下:
1)controller 編寫
位置:cn.j3code.other.api.v1.controller
java複制代碼@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
java複制代碼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
java複制代碼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 如下:
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
java複制代碼@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
java複制代碼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 洩露的操作,但是我留了個問題,就是思考這種方式有什麼問題。
下面是我的思考:
- 使用者上傳的圖檔,通路時每次都會經過本系統,造成了本系統的壓力
- 如果一個頁面需要回顯的圖檔過多,那頁面響應會不會很慢
- 如果系統崩潰了或者服務崩潰了,會導緻圖檔不可通路,但其實第三方 url 是沒有問題的
好吧,其實上面總結就兩個問題,即:性能 和 可用性。
這裡的解決方法是,如果資金充裕而且 COS 做了黑白名單等之類的防禦措施可以直接把 COS 的原始 url 傳回出去,沒必要把圖檔資源壓力給我我們本系統。如果你不是這種情況,那麼就給圖檔通路接口增加部署資源,即更新伺服器增加記憶體和貸款,提高資源通路效率及系統性能。
以上就是本節内容,如果文章的中轉方法有啥不足或者您有什麼意見,歡迎一起讨論研究。