Skip to content

使用 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。

示例代码:

java
/**
 * 获取预上传 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 结构:

txt
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 请求到该地址即可将文件上传到服务器。

bash
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,修改后的代码如下:

java
/**
 * 获取预上传 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

java
import jakarta.activation.FileTypeMap;

String contentType = FileTypeMap.getDefaultFileTypeMap().getContentType(fileName);

这个方法默认情况下是加载这个包下面的 META-INF/mime.types 文件(文件内容如下),如果匹配不到则使用 application/octet-stream

txt
#
# 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