项目背景 :本文以「爪印之约」宠物收养管理系统为例,手把手教你从零开始集成阿里云OSS,实现宠物图片的上传、存储与展示。项目基于 FastAPI + MySQL + 原生JS 构建,适合有 Python Web 开发经验的同学参考。
1. 前置准备:阿里云OSS注册与配置
在写代码之前,需要先在阿里云完成以下步骤:
1.1 开通OSS服务
-
访问 阿里云OSS控制台 并登录(需要手机号实名认证)
-
首次进入会提示开通 OSS 服务,点击开通即可
-
免费额度:每月 5GB 标准存储 + 10GB 外网流出流量,个人项目完全够用




1.2 创建Bucket
-
在OSS控制台点击「创建 Bucket」
-
填写配置:
-
Bucket 名称 :
pet-adoption-xxx(需全局唯一,我用了pet-adoption-zsx) -
区域 :选择离你最近的(我选的是华东1-杭州,对应 Endpoint
oss-cn-hangzhou.aliyuncs.com) -
读写权限 :我设置的是私有,后续通过签名URL来访问图片(更安全)
-
![Bucket创建示意 - 名称和区域是关键配置]


1.3 获取AccessKey
-
鼠标悬停右上角头像 → 点击「AccessKey管理」
-
点击「创建AccessKey」,完成手机验证后获得:
-
AccessKey ID :形如
LTAI5txxxxxxxxxxx -
AccessKey Secret :仅显示一次,务必保存好!
-
⚠️ 安全提示:AccessKey Secret 只在创建时显示一次,请务必复制保存。如果泄漏,可以在控制台禁用并重新创建。





注意:到目前为止你只需要拿到ID和Secret还有一开始创建的Bucket名称即可配置OSS
1.4 配置Bucket跨域(CORS)
如果你的前端要通过浏览器直接上传到OSS(本文走的是后端代理上传,但后续可能用到),建议提前配置:
-
Bucket 列表 → 点击你的 Bucket → 数据安全 → 跨域设置
-
添加规则:
-
来源 :
*(生产环境建议限制为具体域名) -
允许Methods :
GET POST PUT -
允许Headers :
*
-
2. 项目依赖安装
在项目的 requirements.txt 中,OSS 相关依赖有两项:
# requirements.txt(关键依赖)
oss2>=2.18.0 # 阿里云官方 Python SDK
Pillow>=10.0.0 # 图片处理(压缩、裁剪)
python-dotenv>=1.0.0 # 环境变量管理
python-multipart>=0.0.6 # FastAPI 文件上传支持
fastapi>=0.104.1
uvicorn>=0.24.0
sqlalchemy>=2.0.23
pymysql>=1.1.0
安装依赖:
bash
pip install -r requirements.txt
3. 环境变量配置
为了安全性,OSS 的密钥不硬编码在代码中,而是通过 .env 文件管理:
bash
# .env 文件(放在项目根目录 pet_adoption_system/ 下)
# OSS AccessKey ID(在阿里云控制台创建)
OSS_ACCESS_KEY_ID=LTAI5txxxxxxxxxxx
# OSS AccessKey Secret
OSS_ACCESS_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxx
# OSS Bucket名称(创建Bucket时填写的名称)
OSS_BUCKET_NAME=pet-adoption-zsx
# OSS Endpoint(根据Bucket区域选择)
OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
# OSS域名(可选,绑定了CDN自定义域名时填写)
OSS_DOMAIN=
# 签名URL过期时间(秒),默认3600 = 1小时
OSS_EXPIRE_TIME=3600
Endpoint 区域对照表
| 区域 | Endpoint |
|---|---|
| 华东1-杭州 | oss-cn-hangzhou.aliyuncs.com |
| 华东2-上海 | oss-cn-shanghai.aliyuncs.com |
| 华北2-北京 | oss-cn-beijing.aliyuncs.com |
| 华南1-深圳 | oss-cn-shenzhen.aliyuncs.com |
4. 后端:OSS配置模块
创建 config/oss_config.py,从环境变量中读取配置:
python
import os
from dotenv import load_dotenv
load_dotenv()
class OSSConfig:
ACCESS_KEY_ID = os.getenv('OSS_ACCESS_KEY_ID', '')
ACCESS_KEY_SECRET = os.getenv('OSS_ACCESS_KEY_SECRET', '')
BUCKET_NAME = os.getenv('OSS_BUCKET_NAME', '')
ENDPOINT = os.getenv('OSS_ENDPOINT', 'https://oss-cn-hangzhou.aliyuncs.com')
DOMAIN = os.getenv('OSS_DOMAIN', '')
EXPIRE_TIME = int(os.getenv('OSS_EXPIRE_TIME', '3600'))
💡 设计要点 :所有配置项都提供默认值,即使
.env文件缺失也不会导致程序崩溃,而是优雅地提示"OSS未配置"。
5. 后端:OSS服务模块(核心)
这是整个OSS集成的核心------services/oss_service.py ,它封装了初始化、图片处理、上传、签名URL生成、删除等全套能力。
5.1 完整代码
python
import oss2
import time
from io import BytesIO
from PIL import Image
from config.oss_config import OSSConfig
from common.exceptions import UploadError
class OSSService:
def __init__(self):
self.is_configured = bool(
OSSConfig.BUCKET_NAME
and OSSConfig.ACCESS_KEY_ID
and OSSConfig.ACCESS_KEY_SECRET
)
if self.is_configured:
auth = oss2.Auth(OSSConfig.ACCESS_KEY_ID, OSSConfig.ACCESS_KEY_SECRET)
self.bucket = oss2.Bucket(auth, OSSConfig.ENDPOINT, OSSConfig.BUCKET_NAME)
else:
self.bucket = None
def check_configured(self):
if not self.is_configured:
raise UploadError("OSS未配置,请先在.env文件中配置OSS相关参数")
return True
def process_image(self, file_content: bytes, size: int = 400) -> bytes:
"""压缩和裁剪图片为正方形"""
img = Image.open(BytesIO(file_content))
if img.mode != 'RGB':
img = img.convert('RGB')
# 中心裁剪为正方形
width, height = img.size
if width != height:
min_side = min(width, height)
left = (width - min_side) / 2
top = (height - min_side) / 2
right = (width + min_side) / 2
bottom = (height + min_side) / 2
img = img.crop((left, top, right, bottom))
# 缩放到目标尺寸
if width > size or height > size:
img = img.resize((size, size), Image.Resampling.LANCZOS)
# 转换为JPEG并控制质量
output = BytesIO()
img.save(output, format='JPEG', quality=85)
return output.getvalue()
def upload_image(self, file_content: bytes, pet_id: int, filename: str) -> str:
"""上传图片到OSS,返回object_key"""
processed_content = self.process_image(file_content)
object_key = f"pets/{pet_id}/{filename}"
try:
self.bucket.put_object(object_key, processed_content)
return object_key
except Exception as e:
raise UploadError(f"图片上传失败: {str(e)}")
def get_signed_url(self, object_key: str) -> str:
"""获取私有bucket的签名URL"""
if not object_key:
return ''
try:
url = self.bucket.sign_url('GET', object_key, OSSConfig.EXPIRE_TIME)
return url
except Exception as e:
raise UploadError(f"获取图片链接失败: {str(e)}")
def delete_image(self, object_key: str) -> bool:
"""删除OSS中的图片"""
if not object_key:
return False
try:
self.bucket.delete_object(object_key)
return True
except Exception as e:
raise UploadError(f"删除图片失败: {str(e)}")
5.2 核心设计思路
| 模块 | 说明 |
|---|---|
__init__ |
惰性初始化,判断是否有配置才创建 Bucket 客户端,未配置时不报错 |
process_image |
使用 Pillow 做中心裁剪→缩放→JPEG压缩,统一输出 400×400 正方形 |
upload_image |
按 pets/{pet_id}/{timestamp}.{ext} 规则命名,调用 put_object 上传 |
get_signed_url |
私有 Bucket 必须用签名 URL 才能访问,有效期由 EXPIRE_TIME 控制 |
delete_image |
删除宠物时同步清理 OSS 上的图片 |
5.3 为什么选择签名URL而不是公共读?
-
公共读Bucket:图片URL永久有效,但任何人都能访问(包括爬虫、未授权用户)
-
私有Bucket + 签名URL:只有持有签名链接的人才能访问,且可以控制有效期(本项目设为1小时),更安全
每次前端请求宠物列表时,后端动态生成签名URL,过期后自动刷新。
6. 后端:上传接口实现
在 controllers/pet_controller.py 中添加图片上传接口:
python
from fastapi import APIRouter, Depends, UploadFile, File
from sqlalchemy.orm import Session
from database import get_db
from services import pet_service
from services.auth_service import require_admin
router = APIRouter(prefix="/api/pets", tags=["宠物管理"])
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'webp'}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
@router.post("/{pet_id}/upload-image")
def upload_pet_image(
pet_id: int,
file: UploadFile = File(...),
db: Session = Depends(get_db),
admin_user = Depends(require_admin)
):
"""管理员上传宠物图片到OSS"""
# 1. 校验文件扩展名
ext = file.filename.rsplit('.', 1)[-1].lower() if '.' in file.filename else ''
if ext not in ALLOWED_EXTENSIONS:
return {"msg": f"不支持的文件格式,仅支持: {', '.join(ALLOWED_EXTENSIONS)}"}
# 2. 校验文件大小
content = file.file.read()
if len(content) > MAX_FILE_SIZE:
return {"msg": "文件大小不能超过5MB"}
# 3. 生成文件名并上传
filename = f"{int(time.time())}.{ext}"
result = pet_service.upload_pet_image(
db, pet_id=pet_id, file_content=content, filename=filename
)
return {"msg": "上传成功", "image_url": result['image_url']}
接口说明
| 项目 | 内容 |
|---|---|
| 接口路径 | POST /api/pets/{pet_id}/upload-image |
| 权限要求 | 管理员登录(JWT Token) |
| 请求格式 | multipart/form-data,字段名 file |
| 格式限制 | JPG / PNG / GIF / WebP |
| 大小限制 | ≤ 5MB |
| 处理流程 | 校验格式 → 校验大小 → 压缩裁剪 → 上传OSS → 更新数据库 → 返回签名URL |
7. 后端:Service层集成OSS
在 services/pet_service.py 中,有两个关键方法:
7.1 上传并更新数据库
python
def upload_pet_image(db: Session, pet_id: int, file_content: bytes, filename: str) -> dict:
"""上传宠物图片到OSS并更新数据库"""
pet = pet_crud.get_pet_by_id(db, pet_id)
if not pet:
raise NotFoundError("宠物不存在")
oss_service = OSSService()
# 上传图片,返回 object_key (如 pets/5/1715432000.jpg)
object_key = oss_service.upload_image(file_content, pet_id, filename)
# 获取可访问的签名URL
signed_url = oss_service.get_signed_url(object_key)
# 数据库中存储的是 object_key,而非完整URL
# 这样签名过期后可以重新生成,URL更灵活
pet_crud.update_pet(db, pet_id=pet_id, update_data={"image_url": object_key})
return {"object_key": object_key, "image_url": signed_url}
7.2 查询时动态生成签名URL
python
def get_pet_by_id(db: Session, pet_id: int):
pet = pet_crud.get_pet_by_id(db, pet_id=pet_id)
# 如果 image_url 不是外部链接(HTTP开头或/static/开头),说明是OSS的object_key
if pet and pet.image_url and \
not pet.image_url.startswith(('http://', 'https://', '/static/')):
oss_service = OSSService()
if oss_service.is_configured:
pet.image_url = oss_service.get_signed_url(pet.image_url)
return pet
💡 设计亮点 :数据库存的是 object_key(如 pets/5/1715432000.jpg),查询时动态生成签名URL。这样做的好处是:
-
签名URL过期后自动刷新,用户始终能看到图片
-
切换 CDN 域名只需改配置,无需迁移数据库
-
兼容旧数据(外部图片URL以
http://、https://或/static/开头的直接透传)
7.3 数据库模型
python
# models/model.py
class Pet(Base):
__tablename__ = "pets"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String(50), nullable=False)
species = Column(String(20), nullable=False)
breed = Column(String(50), nullable=True)
age = Column(Float, nullable=True)
gender = Column(String(10), nullable=True)
# ... 其他字段 ...
image_url = Column(String(255), nullable=True) # 存的是OSS的object_key
# ...
8. 前端:图片上传组件
在 admin.html 管理后台,管理员可以对每只宠物上传图片:
8.1 HTML结构
html
<!-- 宠物管理表格中,每行有一个"上传图片"按钮 -->
<button class="btn btn-primary"
onclick="showUploadImageForm(${p.id})">
上传图片
</button>
<!-- 上传表单(默认隐藏,点击按钮后展开) -->
<div id="uploadImageForm" style="display:none;">
<h4>上传宠物图片</h4>
<p>支持JPG/PNG/GIF/WebP格式,≤5MB,将自动压缩裁剪为正方形</p>
<div class="form-group">
<label>选择图片</label>
<input type="file" id="imageFile"
accept=".jpg,.jpeg,.png,.gif,.webp"
onchange="previewImage(this)">
</div>
<div id="imagePreview" style="display:none;">
<img id="previewImg" style="width:150px;height:150px;
object-fit:cover;border-radius:8px;">
</div>
<button class="btn btn-success" onclick="uploadImage()"
id="uploadBtn" disabled>确认上传</button>
<button class="btn" onclick="hideUploadImageForm()">取消</button>
</div>
8.2 JavaScript核心逻辑
javascript
let currentUploadPetId = null;
// 展开上传表单
function showUploadImageForm(petId) {
currentUploadPetId = petId;
document.getElementById('uploadImageForm').style.display = 'block';
document.getElementById('imageFile').value = '';
document.getElementById('imagePreview').style.display = 'none';
document.getElementById('uploadBtn').disabled = true;
document.getElementById('uploadImageForm').scrollIntoView({ behavior: 'smooth' });
}
// 图片预览
function previewImage(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
// 前端预校验文件大小
if (file.size > 5 * 1024 * 1024) {
showMessage('petMessage', '文件大小不能超过5MB', 'error');
input.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('previewImg').src = e.target.result;
document.getElementById('imagePreview').style.display = 'block';
document.getElementById('uploadBtn').disabled = false;
};
reader.readAsDataURL(file);
}
}
// 执行上传
async function uploadImage() {
if (!currentUploadPetId) return;
const fileInput = document.getElementById('imageFile');
if (!fileInput.files[0]) {
showMessage('petMessage', '请先选择图片', 'error');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const uploadBtn = document.getElementById('uploadBtn');
uploadBtn.disabled = true;
uploadBtn.textContent = '上传中...';
try {
const response = await fetch(
`${API_BASE}/api/pets/${currentUploadPetId}/upload-image`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${getToken()}` },
body: formData
}
);
const result = await response.json();
if (response.ok) {
showMessage('petMessage', '图片上传成功', 'success');
hideUploadImageForm();
loadPetsForAdmin(); // 刷新列表显示新图片
} else {
showMessage('petMessage', result.detail || '上传失败', 'error');
}
} catch (error) {
showMessage('petMessage', '上传失败: ' + error.message, 'error');
} finally {
uploadBtn.disabled = false;
uploadBtn.textContent = '确认上传';
}
}
⚠️ 注意 :上传图片使用的是原生
fetch而非 axios,因为FormData需要直接传递,axios 的拦截器可能会修改 Content-Type 导致问题。
9. 前端:宠物卡片图片展示优化
在首页 index.html 中,宠物卡片使用 Flex 布局,图片在右侧,信息在左侧:
9.1 卡片的HTML渲染
python
function renderPets(pets) {
const container = document.getElementById('petList');
if (pets.length === 0) {
container.innerHTML = '<p style="text-align:center;">暂无宠物信息</p>';
return;
}
container.innerHTML = pets.map(pet => `
<div class="pet-card" onclick="viewPetDetail(${pet.id})">
<div class="pet-info">
<div class="pet-name">${pet.name}</div>
<div class="pet-details">
${getSpeciesText(pet.species)} ·
${pet.age || '?'}岁 ·
${pet.gender === 'male' ? '♂ 公' : '♀ 母'}
</div>
<span class="badge badge-${pet.status}">
${getStatusText(pet.status)}
</span>
</div>
<img class="pet-card-image"
src="${pet.image_url || '/static/images/default-pet.png'}"
alt="${pet.name}"
onerror="this.src='/static/images/default-pet.png'">
</div>
`).join('');
}
9.2 CSS布局
css
.pet-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
overflow: hidden;
cursor: pointer;
display: flex;
flex-direction: row; /* 水平排列:信息左,图片右 */
min-height: 180px;
transition: all 0.3s;
}
.pet-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}
.pet-card-image {
width: 180px;
min-width: 180px; /* 固定宽度防止被压缩 */
height: 180px; /* 正方形显示 */
object-fit: cover; /* 保持比例裁剪溢出 */
flex-shrink: 0; /* 不允许缩小 */
}
.pet-card .pet-info {
padding: 20px;
flex: 1; /* 自动填充剩余空间 */
display: flex;
flex-direction: column;
justify-content: center;
}
最终效果:


10. 完整的项目结构一览
css
pet_adoption_system/
├── .env # OSS和数据库配置(不提交Git)
├── .env.example # 配置模板(提交Git)
├── main.py # FastAPI入口,启动时自动创建默认管理员
├── settings.py # 应用通用配置
├── database.py # MySQL数据库连接
├── requirements.txt # 依赖清单
│
├── config/
│ └── oss_config.py # 🔑 OSS配置(从.env读取)
│
├── services/
│ ├── oss_service.py # 🔑 OSS核心服务(上传/签名URL/删除/图片处理)
│ ├── pet_service.py # 宠物业务逻辑(调用OSS服务)
│ ├── auth_service.py # 权限验证(require_admin)
│ └── ...
│
├── controllers/
│ ├── pet_controller.py # 🔑 宠物API(含图片上传接口)
│ └── ...
│
├── models/
│ └── model.py # Pet模型(image_url字段存object_key)
│
├── crud/
│ └── pet_crud.py # 数据库增删改查
│
├── common/
│ └── exceptions.py # 自定义异常(UploadError等)
│
└── static/
├── index.html # 首页(宠物卡片展示)
├── admin.html # 🔑 管理后台(图片上传功能)
├── pet_detail.html # 宠物详情
├── css/
│ └── style.css # 卡片布局 + 图片样式
└── js/
└── api.js # Axios封装 + JWT鉴权
11. 踩坑与注意事项
11.1 AccessKey安全
⚠️ 千万不要把 AccessKey Secret 提交到 Git!
-
项目中
.env已加入.gitignore -
提供
.env.example模板供团队成员参考 -
生产环境建议使用 RAM 子账号,只授予 OSS 必要的权限
11.2 Bucket权限选择
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 公共读 | URL永久有效,无需后端处理 | 任何人都能访问,有泄漏风险 | 公开的非敏感图片 |
| 私有 + 签名URL ✅ | 安全性高,可控制有效期 | URL有时效性,需后端生成 | 本项目采用的方案 |
11.3 图片处理策略
-
为什么要处理图片:用户可能上传几MB的高清原图,直接存储浪费空间和流量
-
本项目策略:Pillow + 中心裁剪为正方形 + 缩放至400×400 + JPEG品质85
-
结果:一张5MB的原图处理后通常只有30-80KB,大幅节省存储和带宽成本
11.4 object_key vs 完整URL
数据库存的是 pets/5/1715432000.jpg(object_key),不是 https://xxx.oss-cn-hangzhou.aliyuncs.com/pets/5/...。原因:
-
签名URL有过期时间,存完整URL会过期
-
将来切换 CDN 域名,只需改配置,数据库无需迁移
-
兼容混合数据源(外部链接、本地文件、OSS)
11.5 前后端双重校验
| 校验项 | 前端 | 后端 |
|---|---|---|
| 文件格式 | accept 属性 + JS校验 |
扩展名白名单 |
| 文件大小 | JS校验并阻止选择 | 读取后校验 |
| 权限 | 隐藏按钮 | require_admin 依赖注入 |
11.6 上传接口使用fetch而非axios
上传 FormData 时,使用原生 fetch 而不通过 axios,避免 axios 拦截器自动设置 Content-Type: application/json 覆盖掉 multipart/form-data。
12. 总结
本文完整展示了在一个 FastAPI 宠物收养系统中集成阿里云OSS的全过程:
-
配置层 :通过
.env管理密钥,oss_config.py统一读取 -
服务层 :
oss_service.py封装了图片处理、上传、签名URL生成、删除 -
接口层 :
RESTful API设计,格式/大小/权限三重校验 -
展示层:Flex布局实现卡片式图片展示,签名URL动态生成
整个流程的文件流转图:
css
用户选择图片 → 前端预览 → fetch上传FormData
→ 后端 pet_controller 校验格式/大小
→ oss_service.process_image() 裁剪压缩
→ oss_service.upload_image() 上传OSS
→ pet_crud.update_pet() 更新数据库
→ 返回签名URL给前端
→ 前端刷新列表显示新图片
如果你也在做类似的项目,希望这篇文章能帮你少走弯路,快速完成阿里云OSS的集成!🚀
相关文档: