Supabase中通过腾讯云COS的域名直接访问Storage资源

内容纲要

上一篇→《自托管的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

实施步骤

  1. 创建触发器函数
-- 创建触发器函数
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. 绑定存储策略
-- 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)';
  1. 效果
    当用户上传新头像时,profiles.avatar_url 会自动更新为:
    users/<uuid>/avatar.jpg/fe32380c-2053-42fa-b058-ed0fcdb6cdc9

技术亮点

  • 路径格式验证:确保符合users//avatar.jpg结构
  • 类型安全转换:显式转换UUID防止非法格式
  • 精准触发条件:仅处理avatars存储桶的变更

安全防护体系构建

风险场景

  • 未授权用户篡改路径参数
  • 恶意文件覆盖攻击
  • 匿名用户批量上传

防御方案

  1. 存储桶权限控制
-- 禁用匿名写权限
REVOKE INSERT, UPDATE ON storage.objects FROM anon;
  1. 行级安全策略(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
);
  1. 前端强制身份绑定
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) 额外安全建议

  1. 存储桶权限最小化:

    • 将 avatars 存储桶的 public 权限设置为 仅读,禁止匿名上传。
    • 在 Supabase 控制台中,进入 Storage -> avatars -> Policies,关闭所有匿名写权限。
  2. 日志监控:

    • 在 Supabase 控制台的 Logs -> Storage 中监控异常上传行为。
    • 设置报警规则(如频繁上传失败)。
  3. 文件覆盖限制:

    • 如果允许保留历史头像版本,可禁用 upsert: true,改为生成唯一文件名(如 avatar_.jpg)。