上一篇→《自托管的Supabase如何集成腾讯云COS对象存储?》完成了Supabase与腾讯云COS的集成配置,但在实际使用中发现资源请求需通过Supabase服务器中转,当面临大流量或大文件传输时,服务器带宽极易成为性能瓶颈。本文针对该问题提出直连优化方案,并深入探讨安全防护措施。
直连访问的核心问题分析
传统访问路径
https://supabase.xxx.com/storage/v1/object/public/path/file.jpg
➔ 请求经服务器代理,受限于服务器带宽
COS直连路径
https://xxx.cos.ap-guangzhou.myqcloud.com/bucket/tenant/path/file.jpg/version-uuid
➔ 需解决版本号拼接问题
关键路径解析:xxx.cos.../存储桶/TENANT_ID/文件路径/版本号
综上,要通过COS域名直接访问资源,只需要按照关键路径解析拼接URL即可。
数据库触发器实现版本号同步
方案原理
通过PostgreSQL触发器自动同步storage.objects表的版本号到用户表profiles.avatar_url
实施步骤
- 创建触发器函数
-- 创建触发器函数
CREATE OR REPLACE FUNCTION sync_update_profile_avatar_url()
RETURNS TRIGGER AS $$
DECLARE
path_segments TEXT[] := string_to_array(NEW.name, '/');
target_user_id UUID;
BEGIN
-- 增强路径验证:支持大小写混合
IF array_length(path_segments, 1) >=2
AND path_segments[2] ~ '^avatar\.(jpe?g|png|gif|JPE?G|PNG|GIF)$' -- 正则表达式匹配
THEN
BEGIN
-- 转换用户ID(自动处理大小写)
target_user_id := path_segments[1]::UUID;
-- 空值保护
IF target_user_id IS NOT NULL THEN
UPDATE public.profiles
SET avatar_url = NEW.name || '/' || NEW.version
WHERE id = target_user_id;
END IF;
EXCEPTION
WHEN invalid_text_representation THEN
RAISE NOTICE '路径包含非法UUID: %', path_segments[1];
END;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
- 绑定存储策略
-- 1. 删除旧触发器(如果存在)
DROP TRIGGER IF EXISTS trigger_update_avatar_url ON storage.objects;
-- 2. 创建新触发器
CREATE TRIGGER trigger_update_avatar_url
AFTER INSERT OR UPDATE ON storage.objects
FOR EACH ROW
WHEN (NEW.bucket_id = 'avatars')
EXECUTE FUNCTION sync_update_profile_avatar_url();
-- 3. 添加注释
COMMENT ON TRIGGER trigger_update_avatar_url
ON storage.objects IS '头像URL同步器(支持jpg/png/gif)';
- 效果
当用户上传新头像时,profiles.avatar_url 会自动更新为:
users/<uuid>/avatar.jpg/fe32380c-2053-42fa-b058-ed0fcdb6cdc9
技术亮点
- 路径格式验证:确保符合users/
/avatar.jpg结构 - 类型安全转换:显式转换UUID防止非法格式
- 精准触发条件:仅处理avatars存储桶的变更
安全防护体系构建
风险场景
- 未授权用户篡改路径参数
- 恶意文件覆盖攻击
- 匿名用户批量上传
防御方案
- 存储桶权限控制
-- 禁用匿名写权限
REVOKE INSERT, UPDATE ON storage.objects FROM anon;
- 行级安全策略(RLS)
-- 禁用匿名写权限
CREATE POLICY "用户专属头像管理"
ON storage.objects FOR ALL
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = 'users'
AND (storage.foldername(name))[2] = auth.uid()::text
);
- 前端强制身份绑定
async function uploadAvatar(file) {
const user = supabase.auth.user();
if (!user) throw new Error('Authentication required');
const filePath = `users/${user.id}/avatar.jpg`;
const { error } = await supabase.storage
.from('avatars')
.upload(filePath, file, {
upsert: true,
cacheControl: '3600',
contentType: 'image/jpeg'
});
}
性能优化
1.如果头像更新频繁,触发器可能增加数据库负载,可以添加索引:
CREATE INDEX CONCURRENTLY idx_avatar_objects
ON storage.objects (bucket_id, name text_pattern_ops);
CREATE INDEX idx_objects_bucket_name ON storage.objects (bucket_id, name);
2.路径规范:确保 avatar_url 的路径与 storage.objects.name 严格一致,否则拼接会失败。
3.防止 SQL 注入:如果允许用户自定义路径,需对路径进行合法性校验。
(5) 漏洞分析
如果直接允许客户端任意上传文件到 users/${userId}/avatar.jpg,且无权限校验,攻击者可以:
- 伪造请求中的 userId。
-
覆盖他人头像文件。
解决方案:使用 Supabase Auth 强制绑定用户身份
步骤 1:启用行级安全(RLS)并创建存储策略
在 Supabase 控制台中,进入 Storage -> Policies,为 avatars 存储桶创建上传策略:-- 策略名称:Allow users to manage their own avatars CREATE POLICY "用户只能管理自己的头像" ON storage.objects FOR ALL USING ( -- 路径格式必须是 'users/<auth.uid()>/avatar.jpg' bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'users' AND (storage.foldername(name))[2] = auth.uid()::text );
步骤 2:前端代码集成 Auth
在前端上传头像时,强制使用当前登录用户的 uid,而非客户端传递的 userId:import { supabase } from './supabaseClient'; async function uploadAvatar(file) { // 获取当前登录用户的 ID const user = supabase.auth.user(); if (!user) throw new Error('未登录'); // 路径格式:users/<auth-uid>/avatar.jpg const filePath = `users/${user.id}/avatar.jpg`; // 上传文件(自动受 RLS 策略保护) const { error } = await supabase.storage .from('avatars') .upload(filePath, file, { upsert: true }); if (error) throw error; }
效果:
- 攻击者无法伪造他人 uid(因为 auth.uid() 由 Supabase Auth 自动注入)。
- 非本人上传时,RLS 策略会拒绝操作(返回 403 Forbidden)。
(6) 额外安全建议
-
存储桶权限最小化:
- 将 avatars 存储桶的 public 权限设置为 仅读,禁止匿名上传。
- 在 Supabase 控制台中,进入 Storage -> avatars -> Policies,关闭所有匿名写权限。
-
日志监控:
- 在 Supabase 控制台的 Logs -> Storage 中监控异常上传行为。
- 设置报警规则(如频繁上传失败)。
-
文件覆盖限制:
- 如果允许保留历史头像版本,可禁用 upsert: true,改为生成唯一文件名(如 avatar_
.jpg)。
- 如果允许保留历史头像版本,可禁用 upsert: true,改为生成唯一文件名(如 avatar_