预签名 URL
预签名 URL (Presigned URL) 是一种临时授权访问 S3 对象的机制,无需公开 Bucket 或对象权限。
🔑 什么是预签名 URL
预签名 URL 包含临时签名信息,允许在指定时间内访问私有对象。
常规 URL:
https://my-bucket.s3.amazonaws.com/photo.jpg ❌ 403 Forbidden
预签名 URL:
https://my-bucket.s3.amazonaws.com/photo.jpg?
AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&
Expires=1234567890&
Signature=signature_string ✅ 200 OK🎯 使用场景
1. 临时下载权限
场景: 用户购买数字产品
└─> 生成临时下载链接
└─> 链接1小时后失效2. 安全文件上传
场景: 用户上传头像
└─> 前端获取预签名 URL
└─> 直接上传到 S3
└─> 不经过后端服务器3. 分享私有文件
场景: 分享私密文档给特定用户
└─> 生成限时链接
└─> 无需修改权限⬇️ 下载对象的预签名 URL
Node.js
javascript
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
// 生成下载 URL (GET)
const getPresignedUrl = (bucketName, key, expiresIn = 3600) => {
const params = {
Bucket: bucketName,
Key: key,
Expires: expiresIn // 秒,默认 3600 (1小时)
};
const url = s3.getSignedUrl('getObject', params);
return url;
};
// 使用示例
const url = getPresignedUrl('my-bucket', 'document.pdf', 3600);
console.log('Download URL:', url);
// 浏览器中直接访问这个 URL 即可下载
// <a href="${url}" download>Download</a>
// 使用 Promise
const getPresignedUrlAsync = (bucketName, key, expiresIn = 3600) => {
return new Promise((resolve, reject) => {
s3.getSignedUrl('getObject', {
Bucket: bucketName,
Key: key,
Expires: expiresIn
}, (err, url) => {
if (err) reject(err);
else resolve(url);
});
});
};Express API 示例
javascript
const express = require('express');
const app = express();
// 生成下载链接的 API
app.get('/api/files/:fileId/download-url', async (req, res) => {
const { fileId } = req.params;
// 从数据库获取文件信息
const file = await db.files.findOne({ id: fileId, userId: req.user.id });
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// 生成预签名 URL
const url = getPresignedUrl('my-bucket', file.s3Key, 3600);
res.json({
url: url,
expiresIn: 3600,
expiresAt: new Date(Date.now() + 3600 * 1000)
});
});
app.listen(3000);Python
python
import boto3
from botocore.exceptions import ClientError
from datetime import datetime, timedelta
s3 = boto3.client('s3')
def generate_presigned_url(bucket_name, object_key, expiration=3600):
"""生成下载 URL"""
try:
url = s3.generate_presigned_url(
'get_object',
Params={
'Bucket': bucket_name,
'Key': object_key
},
ExpiresIn=expiration
)
return url
except ClientError as e:
print(f"Error: {e}")
return None
# 使用示例
url = generate_presigned_url('my-bucket', 'document.pdf', 3600)
print(f"Download URL: {url}")设置下载文件名
javascript
const getPresignedUrlWithFilename = (bucketName, key, filename, expiresIn = 3600) => {
const params = {
Bucket: bucketName,
Key: key,
Expires: expiresIn,
ResponseContentDisposition: `attachment; filename="${filename}"`
};
return s3.getSignedUrl('getObject', params);
};
// 下载时文件名显示为 "我的文档.pdf"
const url = getPresignedUrlWithFilename(
'my-bucket',
'documents/abc123.pdf',
'我的文档.pdf'
);⬆️ 上传对象的预签名 URL
后端生成上传 URL
javascript
// 生成上传 URL (PUT)
const getUploadPresignedUrl = (bucketName, key, contentType, expiresIn = 300) => {
const params = {
Bucket: bucketName,
Key: key,
Expires: expiresIn, // 上传链接一般设置较短时间
ContentType: contentType
};
const url = s3.getSignedUrl('putObject', params);
return url;
};
// Express API
app.post('/api/upload/presigned-url', async (req, res) => {
const { filename, contentType } = req.body;
const userId = req.user.id;
// 生成唯一的 S3 key
const key = `uploads/${userId}/${Date.now()}-${filename}`;
// 生成上传 URL
const url = getUploadPresignedUrl('my-bucket', key, contentType, 300);
// 保存到数据库
await db.files.create({
userId: userId,
filename: filename,
s3Key: key,
status: 'pending'
});
res.json({
uploadUrl: url,
key: key,
expiresIn: 300
});
});前端直传实现
javascript
// React 示例
async function uploadFile(file) {
try {
// 1. 获取预签名 URL
const response = await fetch('/api/upload/presigned-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type
})
});
const { uploadUrl, key } = await response.json();
// 2. 使用预签名 URL 上传到 S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type
}
});
// 3. 通知后端上传完成
await fetch('/api/upload/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key })
});
console.log('Upload successful!');
} catch (err) {
console.error('Upload failed:', err);
}
}
// 使用示例
<input type="file" onChange={(e) => uploadFile(e.target.files[0])} />带进度的上传
javascript
async function uploadFileWithProgress(file, onProgress) {
// 1. 获取预签名 URL
const response = await fetch('/api/upload/presigned-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type
})
});
const { uploadUrl } = await response.json();
// 2. 使用 XMLHttpRequest 上传以支持进度
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentage = (e.loaded / e.total) * 100;
onProgress(percentage);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Upload failed'));
});
xhr.open('PUT', uploadUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
});
}
// 使用
uploadFileWithProgress(file, (progress) => {
console.log(`Upload progress: ${progress.toFixed(2)}%`);
});📝 POST 表单上传
使用 POST 方法和表单上传,支持额外的条件。
javascript
// 后端生成 POST 表单数据
const getPostPresignedUrl = (bucketName, key, fields = {}) => {
return new Promise((resolve, reject) => {
const params = {
Bucket: bucketName,
Fields: {
key: key,
...fields
},
Expires: 300,
Conditions: [
['content-length-range', 0, 10485760], // 最大 10MB
['starts-with', '$Content-Type', 'image/'] // 只允许图片
]
};
s3.createPresignedPost(params, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
};
// Express API
app.post('/api/upload/post-presigned', async (req, res) => {
const { filename } = req.body;
const key = `uploads/${Date.now()}-${filename}`;
const data = await getPostPresignedUrl('my-bucket', key);
res.json({
url: data.url,
fields: data.fields
});
});前端表单上传
javascript
async function uploadWithForm(file) {
// 1. 获取表单数据
const response = await fetch('/api/upload/post-presigned', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name })
});
const { url, fields } = await response.json();
// 2. 构造表单
const formData = new FormData();
// 添加字段(顺序很重要,file 必须最后)
Object.entries(fields).forEach(([key, value]) => {
formData.append(key, value);
});
formData.append('file', file);
// 3. 上传
await fetch(url, {
method: 'POST',
body: formData
});
console.log('Upload successful!');
}⏰ 设置过期时间
javascript
// 不同场景使用不同的过期时间
// 临时下载链接 - 5分钟
const shortUrl = getPresignedUrl('my-bucket', 'temp.pdf', 300);
// 文件分享链接 - 1小时
const shareUrl = getPresignedUrl('my-bucket', 'shared.pdf', 3600);
// 付费内容下载 - 24小时
const paidUrl = getPresignedUrl('my-bucket', 'course.mp4', 86400);
// 最大 7 天
const maxUrl = getPresignedUrl('my-bucket', 'archive.zip', 604800);🔒 安全性增强
1. 限制 IP 地址
javascript
// 使用 Bucket Policy 限制 IP
// 预签名 URL 会继承 Bucket Policy2. 添加自定义条件
javascript
const getSecurePresignedUrl = (bucketName, key, userId) => {
const params = {
Bucket: bucketName,
Key: key,
Expires: 3600,
// 添加自定义元数据条件
Metadata: {
'x-amz-meta-user-id': userId
}
};
return s3.getSignedUrl('getObject', params);
};3. 单次使用链接
javascript
// 实现单次使用链接
const generateOneTimeUrl = async (bucketName, key, userId) => {
const token = crypto.randomBytes(32).toString('hex');
// 存储 token
await db.tokens.create({
token: token,
userId: userId,
s3Key: key,
used: false,
expiresAt: new Date(Date.now() + 3600000)
});
return `/api/download/${token}`;
};
// API 端点
app.get('/api/download/:token', async (req, res) => {
const tokenRecord = await db.tokens.findOne({
token: req.params.token,
used: false,
expiresAt: { $gt: new Date() }
});
if (!tokenRecord) {
return res.status(404).json({ error: 'Invalid or expired token' });
}
// 标记为已使用
await db.tokens.updateOne(
{ _id: tokenRecord._id },
{ $set: { used: true } }
);
// 生成预签名 URL 并重定向
const url = getPresignedUrl('my-bucket', tokenRecord.s3Key, 60);
res.redirect(url);
});💡 最佳实践
1. 设置合理的过期时间
javascript
// ✅ 好:根据场景设置
const uploadUrl = getUploadPresignedUrl(bucket, key, contentType, 300); // 5分钟
const downloadUrl = getPresignedUrl(bucket, key, 3600); // 1小时
// ❌ 坏:过长的过期时间
const url = getPresignedUrl(bucket, key, 604800); // 7天,安全风险2. 验证文件类型
javascript
// 上传时验证 Content-Type
app.post('/api/upload/presigned-url', (req, res) => {
const { contentType } = req.body;
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(contentType)) {
return res.status(400).json({ error: 'Invalid file type' });
}
// 生成 URL...
});3. 限制文件大小
javascript
const getUploadPresignedUrlWithLimit = (bucketName, key, maxSize = 5242880) => {
return new Promise((resolve, reject) => {
const params = {
Bucket: bucketName,
Fields: { key },
Expires: 300,
Conditions: [
['content-length-range', 0, maxSize] // 最大 5MB
]
};
s3.createPresignedPost(params, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
};4. 记录访问日志
javascript
app.post('/api/files/:fileId/download-url', async (req, res) => {
const { fileId } = req.params;
// 生成 URL
const url = getPresignedUrl('my-bucket', key, 3600);
// 记录访问日志
await db.accessLogs.create({
fileId: fileId,
userId: req.user.id,
action: 'download_url_generated',
ip: req.ip,
userAgent: req.get('user-agent'),
timestamp: new Date()
});
res.json({ url });
});📖 相关资源
下一步:学习 生命周期管理 ♻️