使用 S3 协议从前端上传文件到 OSS
🏷️ OSS
本来是通过后端服务中转来上传文件到 OSS 的,但是为了降低后端服务的带宽流量和服务器资源消耗,决定改为通过前端直接上传到。
通过前端上传时必须要考虑鉴权的问题,公司使用的主要是七牛云,之前的项目中使用过通过 项目凭证 的方式:后端接口给前端提供一次性且带有效期的令牌,前端使用该令牌上传文件。
当前项目有些不一样,使用了 AWS S3 协议来兼容多个品牌的对象存储,所以最好是修改后仍然能兼容其它品牌。
文心一言:
对于 S3 上传,AWS 通常使用 预签名 URL 或 签名策略 来实现。这些签名机制通常涉及 AWS Signature Version 4,这是一种用于对 AWS 请求进行身份验证的机制。
如果你想要生成一个预签名的 S3 URL 用于上传,你应该使用 aws-java-sdk-s3
库中的相关类。例如,你可以使用 AmazonS3
客户端的 generatePresignedUrl
方法来生成一个预签名的 PUT 请求 URL,该 URL 允许用户在指定的时间范围内上传文件到 S3。
根据文心一言的回答,有两种实现方式:预签名 URL 和 签名策略 。
签名策略 的方案感觉有些类似于七牛云的项目凭证,但是没有找到具体的 API 和示例代码。
七牛云的开发者文档(AWS SDK for Java)中提供了 预签名 URL 方式的示例代码,只是使用的依赖和项目中的不一样。项目中使用的是 aws-java-sdk-s3:1.12.540 ,在 com.amazonaws.services.s3.AmazonS3
类中提供了 generatePresignedUrl
方法来生成预签名的 URL。
示例代码:
/**
* 获取预上传 URL 链接
*
* @param objectKey 对象 KEY
* @param second 授权时间
*/
public String getPreuploadUrl(String objectKey, Integer second) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
.withMethod(HttpMethod.PUT)
.withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
return url.toString();
}
七牛云获取的预签名 URL 结构:
https://test-domain.s3.cn-south-1.qiniucs.com/test/2024/05/30/cab0fe332cd843289142807feb2512e2.svg
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Date=20240530T092929Z
&X-Amz-SignedHeaders=host
&X-Amz-Expires=1199
&X-Amz-Credential=azq7eaI1pgl4N78TRBmV4zdV5WH-_U9QI52jn12d%2F20240530%2F%2Fs3%2Faws4_request
&X-Amz-Signature=0db13fda7b4982a4c2704b79e84db4215bcdb1457ab0013da1c9954c76bad35d
最后前端通过发送 PUT 请求到该地址即可将文件上传到服务器。
curl -X PUT --upload-file "<path/to/file>" "<presigned url>"
2024-08-09 追记
最近发现直接打开图片地址时,没做作为图片展示,而是展示一长串乱码的字符,但是在页面中通过 img
标签的 src
属性直接加载图片地址是可以正常展示图片的。
最后发现是对象存储中的文件类型不对,正常应该为 image/jpeg
,但实却是 application/json
(按理正常情况下二进制流上传时应该为 application/octet-stream
才对,不知道为什么是 application/json
,怀疑是前端上传时指定了这个 Content-Type
)。
好在创建预签名 URL 时支持指定 Content-Type
,修改后的代码如下:
/**
* 获取预上传 URL 链接
*
* @param objectKey 对象 KEY
* @param second 授权时间
* @param contentType Content-Type
*/
public String getPreuploadUrl(String objectKey, Integer second, String contentType) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
.withMethod(HttpMethod.PUT)
.withContentType(contentType)
.withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
return url.toString();
}
另外 Java 中可以通过如下代码根据文件名获取 Content-Type
:
import jakarta.activation.FileTypeMap;
String contentType = FileTypeMap.getDefaultFileTypeMap().getContentType(fileName);
这个方法默认情况下是加载这个包下面的 META-INF/mime.types
文件(文件内容如下),如果匹配不到则使用 application/octet-stream
。
#
# Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Distribution License v. 1.0, which is available at
# http://www.eclipse.org/org/documents/edl-v10.php.
#
# SPDX-License-Identifier: BSD-3-Clause
#
#
# A simple, old format, mime.types file
#
text/html html htm HTML HTM
text/plain txt text TXT TEXT
image/gif gif GIF
image/ief ief
image/jpeg jpeg jpg jpe JPG
image/tiff tiff tif
image/png png PNG
image/x-xwindowdump xwd
application/postscript ai eps ps
application/rtf rtf
application/x-tex tex
application/x-texinfo texinfo texi
application/x-troff t tr roff
audio/basic au
audio/midi midi mid
audio/x-aifc aifc
audio/x-aiff aif aiff
audio/x-mpeg mpeg mpg
audio/x-wav wav
video/mpeg mpeg mpg mpe
video/quicktime qt mov
video/x-msvideo avi