Postgres/Supabase含关注计数器系统的安全策略设计咨询
Postgres/Supabase关注计数器场景的最优安全方案
核心结论:不要拆分计数器到独立表,也不要直接配置列级写权限,采用「Security Definer封装事务函数+收敛表直接写权限」的方案是兼顾安全、性能、维护成本的最优解。
方案选型说明
为什么不选拆表方案
- 计数器本身就是用户维度的强关联属性,拆表属于无业务价值的过度设计,后续所有用户信息查询都要额外做一次表关联,平白增加查询开销
- 拆表后依然要解决计数器的修改权限问题,并没有从根源降低安全设计的复杂度,反而多了一份表间一致性维护成本
为什么不直接开放列级安全策略
列级RLS仅能约束「哪些角色可以修改对应列」,无法限制修改逻辑:一旦给普通用户开放follower_counter列的直接写权限,你没法通过策略限制用户只能对计数器做+1/-1的合法操作,恶意用户可以直接把任意用户的粉丝数改成任意数值,数据完全不可信。
具体实现步骤
1. 封装原子操作的安全函数
把插入关注关系、更新计数器的逻辑全部封装到单个带内置校验的函数中,函数配置为SECURITY DEFINER——执行时使用函数创建者(表所有者)的权限运行,不需要给调用用户开放两张表的直接写权限,从根源避免越权。
函数不要信任前端传入的关注者ID,直接从Supabase内置的认证上下文取当前登录用户ID作为操作主体,完全杜绝身份伪造。
参考实现代码:
-- 关注用户函数 CREATE OR REPLACE FUNCTION follow_user(target_following_id uuid) RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE current_user_id uuid := auth.uid(); BEGIN -- 基础校验:未登录拦截 IF current_user_id IS NULL THEN RAISE EXCEPTION '请先登录后再操作'; END IF; -- 校验:禁止关注自己 IF current_user_id = target_following_id THEN RAISE EXCEPTION '无法关注自己'; END IF; -- 校验:禁止重复关注 IF EXISTS ( SELECT 1 FROM following_relationship WHERE follower = current_user_id AND following = target_following_id ) THEN RAISE EXCEPTION '你已关注该用户'; END IF; -- 同事务执行两个操作,原子性保证要么全成功要么全回滚 INSERT INTO following_relationship(follower, following) VALUES (current_user_id, target_following_id); UPDATE users SET follower_counter = follower_counter + 1 WHERE uuid = target_following_id; END; $$; -- 给登录用户开放函数执行权限 GRANT EXECUTE ON FUNCTION follow_user(uuid) TO authenticated;
取关逻辑按照同样的模式封装即可,内部校验当前用户是对应关注关系的发起者,删除关系后给对应粉丝计数器-1。
2. 表级RLS配置
- 给
users表开启RLS:配置策略仅允许用户修改自身行的name等个人字段,收回authenticated角色对follower_counter列的直接修改权限,该字段的所有变更只能通过上面封装的关注/取关函数执行 - 给
following_relationship表开启RLS:允许用户查询公开的关注关系、删除自己发起的关注记录,收回authenticated角色对该表的直接插入权限,所有关注关系的创建只能走封装函数 - 不需要额外给事务传递权限类参数,所有身份校验在函数内部通过
auth.uid()完成,仅需要前端传入被关注用户的ID一个业务参数即可。
3. 函数权限注意点
- 创建
SECURITY DEFINER函数时要使用表所有者账号(通常是postgres角色)创建,不要用普通用户账号 - 函数内部一定要做全链路的参数合法性校验,不要省略重复关注、自我关注这类拦截逻辑,避免产生脏数据
- 不要给函数设置过高的权限,函数内仅保留操作对应两张表的必要权限即可
内容的提问来源于stack exchange,提问作者tobias hassebrock
相关产品推荐
相关产品推荐

