1. 问题背景与核心思路
最近在项目开发中遇到一个典型场景:需要将图片存储功能从业务服务器剥离,但又不想因此影响前后端的正常通信。最初尝试直接将七牛云绑定到主域名,结果导致前端请求全部被导向七牛云,整个系统陷入瘫痪。这个教训让我意识到:云存储服务的集成需要更精细化的设计。
核心诉求其实很明确:
- 图片上传/存储交给七牛云处理
- 前后端通信保持原有架构不变
- 不使用二级域名承载业务逻辑
- 确保HTTPS安全访问
经过多次实践验证,最终形成的解决方案可以概括为:主域名坚守业务阵地,子域名专司图片职责。具体来说:
- 主域名(如example.com)解析回业务服务器,保障前后端正常通信
- 新建img.example.com子域名专门指向七牛云
- 后端提供安全的token生成接口
- 前端仅改造图片上传逻辑
重要提示:千万不要在七牛云控制台绑定业务主域名!这是导致前后端通信中断的罪魁祸首。
2. 环境准备与基础配置
2.1 域名解析调整
首先需要纠正最初的错误配置:
bash复制# 检查当前域名解析情况
dig example.com +short
dig www.example.com +short
如果发现返回的是七牛云的CNAME地址,必须立即修正:
- 登录域名注册商控制台(以阿里云为例)
- 进入「域名解析」页面
- 将@和www记录的CNAME改为A记录,指向业务服务器IP
- TTL建议设置为600秒(10分钟)
markdown复制记录类型对照表:
| 记录类型 | 主机记录 | 记录值 | 说明 |
|----------|----------|-----------------|----------------------|
| A | @ | 1.2.3.4 | 主域名解析 |
| A | www | 1.2.3.4 | www子域名解析 |
| CNAME | img | xxxx.qiniudns.com| 图片子域名(暂不设置)|
2.2 七牛云基础配置
在七牛云控制台需要获取以下关键信息:
- 进入「密钥管理」获取AK/SK
- 在目标存储空间的「空间概览」记录:
- 存储空间名称
- 存储区域代号(如z2表示华南)
- 测试域名(临时使用)
javascript复制// 存储区域与上传域名对应关系
const uploadDomainMap = {
z0: 'up.qiniup.com', // 华东
z1: 'up-z1.qiniup.com', // 华北
z2: 'up-z2.qiniup.com', // 华南
na0: 'up-na0.qiniup.com'// 北美
}
3. 后端Token生成服务实现
3.1 Node.js实现方案
javascript复制const qiniu = require('qiniu');
const express = require('express');
const app = express();
// 配置中间件
app.use(express.json());
// 七牛云配置
const qiniuConfig = {
accessKey: process.env.QINIU_AK,
secretKey: process.env.QINIU_SK,
bucket: 'your-bucket-name',
zone: 'z2' // 根据实际区域修改
};
// 生成上传token
app.get('/api/qiniu/token', (req, res) => {
const mac = new qiniu.auth.digest.Mac(
qiniuConfig.accessKey,
qiniuConfig.secretKey
);
const options = {
scope: qiniuConfig.bucket,
expires: 7200, // 2小时有效期
returnBody: '{"key":"$(key)","hash":"$(etag)","size":$(fsize)}'
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
res.json({
token: uploadToken,
uploadDomain: uploadDomainMap[qiniuConfig.zone],
cdnDomain: 'img.example.com' // 最终访问域名
});
});
// 安全建议:建议添加速率限制和JWT验证
const rateLimit = require('express-rate-limit');
app.use('/api/qiniu/token', rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100 // 每个IP最多100次请求
}));
3.2 Java Spring Boot实现
java复制@RestController
@RequestMapping("/api/qiniu")
public class QiniuController {
@Value("${qiniu.access-key}")
private String accessKey;
@Value("${qiniu.secret-key}")
private String secretKey;
@Value("${qiniu.bucket}")
private String bucket;
@GetMapping("/token")
public Map<String, String> getUploadToken() {
Auth auth = Auth.create(accessKey, secretKey);
StringMap policy = new StringMap()
.put("returnBody", "{\"key\":\"$(key)\",\"hash\":\"$(etag)\"}");
return Map.of(
"token", auth.uploadToken(bucket, null, 3600, policy),
"uploadDomain", "up-z2.qiniup.com",
"cdnDomain", "img.example.com"
);
}
}
4. 前端集成方案
4.1 基础上传实现
javascript复制async function uploadFile(file) {
try {
// 1. 获取上传凭证
const { token, uploadDomain, cdnDomain } = await fetchToken();
// 2. 构造FormData
const formData = new FormData();
formData.append('file', file);
formData.append('token', token);
formData.append('key', generateFileName(file));
// 3. 执行上传
const response = await fetch(`https://${uploadDomain}`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.key) {
return `https://${cdnDomain}/${result.key}`;
}
throw new Error('Upload failed');
} catch (error) {
console.error('Upload error:', error);
throw error;
}
}
// 生成随机文件名
function generateFileName(file) {
const ext = file.name.split('.').pop();
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${ext}`;
}
4.2 高级功能实现
图片压缩上传
javascript复制async function compressAndUpload(file, quality = 0.8) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 计算缩放尺寸
const maxWidth = 1920;
const maxHeight = 1080;
let width = img.width;
let height = img.height;
if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(
maxWidth / width,
maxHeight / height
);
width *= ratio;
height *= ratio;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(async (blob) => {
const compressedFile = new File([blob], file.name, {
type: 'image/jpeg',
lastModified: Date.now()
});
const url = await uploadFile(compressedFile);
resolve(url);
}, 'image/jpeg', quality);
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
}
上传进度监控
javascript复制function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
onProgress(percent);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('Upload failed'));
}
});
xhr.addEventListener('error', () => reject(new Error('Upload error')));
fetchToken().then(({ token, uploadDomain }) => {
const formData = new FormData();
formData.append('file', file);
formData.append('token', token);
xhr.open('POST', `https://${uploadDomain}`, true);
xhr.send(formData);
});
});
}
5. 子域名配置全流程
5.1 DNS解析配置
- 登录域名控制台添加CNAME记录:
- 主机记录:img
- 记录类型:CNAME
- 记录值:七牛云提供的域名(如xxxx.qiniudns.com)
- TTL:建议300秒
bash复制# 验证解析是否生效
dig img.example.com +short
5.2 七牛云域名绑定
- 进入七牛云控制台 → 对象存储 → 空间管理 → 域名管理
- 点击「绑定域名」按钮
- 输入域名:img.example.com
- 选择对应的存储区域
- 获取系统生成的CNAME地址
特别注意:绑定域名后需要到域名注册商处将CNAME记录值更新为七牛云提供的地址
5.3 SSL证书配置
方案一:使用七牛云托管证书
- 在七牛云「SSL证书服务」中申请免费证书
- 填写img.example.com域名
- 按照DNS验证要求添加TXT记录
- 等待证书签发(通常10-30分钟)
- 在域名管理页面关联证书
方案二:上传自有证书
bash复制# 使用Certbot生成证书(需要先确保解析生效)
sudo certbot certonly --nginx -d example.com -d www.example.com -d img.example.com
# 导出证书文件
sudo cat /etc/letsencrypt/live/example.com/fullchain.pem > qiniu_cert.pem
sudo cat /etc/letsencrypt/live/example.com/privkey.pem > qiniu_key.pem
然后在七牛云控制台上传这两个文件的内容。
6. Nginx高级配置
6.1 基础反向代理配置
nginx复制server {
listen 443 ssl;
server_name img.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass https://xyz.qiniudns.com;
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
# 缓存优化
proxy_cache qiniu_cache;
proxy_cache_valid 200 304 12h;
proxy_cache_key "$scheme://$host$request_uri";
}
}
# 缓存路径配置
proxy_cache_path /var/cache/nginx/qiniu levels=1:2 keys_zone=qiniu_cache:10m inactive=24h use_temp_path=off;
6.2 安全加固配置
nginx复制# 限制HTTP方法
if ($request_method !~ ^(GET|HEAD)$ ) {
return 405;
}
# 防盗链配置
location ~* \.(jpg|jpeg|png|gif|webp)$ {
valid_referers none blocked example.com *.example.com;
if ($invalid_referer) {
return 403;
# 或者重写到警告图片
# rewrite ^ /anti-hotlink.jpg break;
}
}
# 图片处理规则
location ~* /(.*)@(.*)\.(jpg|png|gif)$ {
proxy_pass https://xyz.qiniudns.com/$1@$2.$3;
image_filter resize $arg_w $arg_h;
image_filter_jpeg_quality 85;
}
7. 实战问题排查指南
7.1 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 上传返回401 | Token过期或无效 | 检查AK/SK配置,确认token有效期 |
| 图片访问404 | 文件不存在或域名未绑定 | 检查七牛云文件列表,验证域名绑定状态 |
| HTTPS证书警告 | 证书配置错误 | 检查证书链完整性,确认包含中间证书 |
| 上传速度慢 | 区域选择不当 | 确认存储区域与用户主要分布区域匹配 |
| 跨域问题 | CORS未配置 | 在七牛云空间设置中添加CORS规则 |
7.2 日志分析技巧
bash复制# 查看Nginx访问日志
tail -f /var/log/nginx/access.log | grep img.example.com
# 七牛云日志分析(需要先开启日志下载)
qshell cdnlog fetch <bucket> ./logs
zgrep "404" qiniu-log-*.gz
# 实时监控上传错误
journalctl -u your-backend-service -f | grep "Qiniu"
7.3 性能优化建议
-
客户端优化:
- 实现分片上传(大于10MB的文件)
- 添加图片压缩预处理
- 使用Web Worker处理上传任务
-
服务端优化:
- 实现token缓存(减少重复生成开销)
- 添加请求限流保护
- 使用HTTP/2协议
-
CDN优化:
- 配置合适的缓存策略
- 启用智能压缩
- 设置边缘规则(如WebP自动转换)
8. 扩展应用场景
8.1 视频上传方案
javascript复制// 大文件分片上传实现
async function uploadLargeFile(file, {chunkSize = 5 * 1024 * 1024} = {}) {
const { token, uploadDomain } = await fetchToken();
const chunks = Math.ceil(file.size / chunkSize);
const ctxs = [];
for (let i = 0; i < chunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
const formData = new FormData();
formData.append('file', chunk);
formData.append('token', token);
formData.append('key', file.name);
formData.append('chunk', i);
formData.append('chunks', chunks);
const { data } = await axios.post(
`https://${uploadDomain}`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
ctxs.push(data.ctx);
}
// 执行合并
const { data } = await axios.post(
`https://${uploadDomain}/mkfile/${file.size}/key/${encodeURIComponent(file.name)}`,
ctxs.join(','),
{ headers: { 'Content-Type': 'text/plain', 'Authorization': `UpToken ${token}` } }
);
return `https://img.example.com/${data.key}`;
}
8.2 私有空间访问控制
javascript复制// 生成私有文件访问URL
function generatePrivateUrl(key, expires = 3600) {
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const config = new qiniu.conf.Config();
const bucketManager = new qiniu.rs.BucketManager(mac, config);
return bucketManager.privateDownloadUrl(
`https://img.example.com/${key}`,
expires
);
}
8.3 图片处理管道
nginx复制location ~* ^/pipeline/(.*)$ {
proxy_pass https://xyz.qiniudns.com/$1?imageMogr2/thumbnail/500x500/format/webp;
proxy_set_header Host $proxy_host;
}
这套方案经过多个项目的实战检验,在保证系统架构清晰的同时,实现了图片存储的专业化处理。特别是在电商、社交类应用中,能够有效减轻主服务器负载,提升图片相关操作的性能表现。