惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
Hacker News - Newest:
Hacker News - Newest: "LLM"
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
Last Week in AI
Last Week in AI
爱范儿
爱范儿
美团技术团队
V
Visual Studio Blog
P
Proofpoint News Feed
GbyAI
GbyAI
Y
Y Combinator Blog
博客园 - 司徒正美
IT之家
IT之家
Google DeepMind News
Google DeepMind News
F
Full Disclosure
aimingoo的专栏
aimingoo的专栏
宝玉的分享
宝玉的分享
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园_首页
M
MIT News - Artificial intelligence
V
V2EX
C
CXSECURITY Database RSS Feed - CXSecurity.com
A
Arctic Wolf
B
Blog
P
Proofpoint News Feed
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
SecWiki News
SecWiki News
I
Intezer
P
Palo Alto Networks Blog
S
Security Affairs
L
LangChain Blog
C
Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Martin Fowler
Martin Fowler
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
Spread Privacy
Spread Privacy
H
Heimdal Security Blog
有赞技术团队
有赞技术团队
量子位
D
Docker
S
Secure Thoughts
N
News | PayPal Newsroom
The Last Watchdog
The Last Watchdog
H
Hacker News: Front Page
H
Hackread – Cybersecurity News, Data Breaches, AI and More

东东's Blog

Memos: MacOS 下编译安装 Aseprite 脚本 日本关西系列|Day 2 京都的半日闲逛 美国Apple官网购买礼品卡订阅 ChatGPT Plus 北京・中关村森林公园(2026) 日本关西系列|Day 1 抵达临空城与大阪首日 Memos: 查询 Apple ID 注册时间 Memos: Ghostty 开箱即用配置 代码考古:用 gitcharts 挖掘 Git 仓库的演变轨迹 烹饪日记:香煎罗非鱼 Memos: 新时代程序员的顶级焦虑 Memos: 博客新增图文布局和轮播图效果支持 Memos: 最适合空气炸锅烤着吃的红薯品类 Memos: 记录「95分」好吃的‘小帅香菇面’ 妻子爷爷的‘朝鲜军功奖章’ Memos: 博客切换为 Shiki 代码高亮方案 Memos: 博客新增划线、重点及荧光笔效果支持 记录博客字体分包与字体子集化 Memos: 邻座吃饭的一家三口 人在囧途之哈囧 哈尔滨・乡村的冬季 查看香烟生产日期 哈尔滨・东北虎林园 Memos: 来自日本的 ndjp 提供免费的三级子域名 Memos: 刚听说 autojump, 真的好用 Memos: 体验 OpenCode + Superpowers + GPT 5.2 开发需求 Memos: 杰我睿爆雷 GoReleaser 自动发布 Go 镜像到 DockerHub & GitHub Release 初识 Volta & Corepack 前端版本管理工具 部署 Beszel 把 “小鸡们” 归拢起来 Memos: Claude Code in Action 中文版教程 2025 年度回顾 Memos: 体验 tanaos-text-anonymizer-v1 NER 模型 Memos: 查询 Google 账号注册时间 Memos: 关于 Z30 在室内摄像被手机降维打击这点儿事儿 阅读《我与地坛》 Memos: Ghostty + Neovim + LazyVim Memos: 找到 Cursor 运行巨慢的一个原因 Memos: 京东家政 哈尔滨灵活就业人员医保退休待遇申领条件 记地暖不热的维修过程 Memos: 赛博菩萨 Cloudflare 又挂了 AnyTLS 软件的配置与使用 阅读《在巴东》 Memos: Web Archive 暂时离线 忆时光:十五年前我的家(动迁前夕) macOS 系统部署 Valkey 集群模式 阅读《一个名叫欧维的男人决定去死》 Memos: Cursor 服务故障部分功能不可用 阅读《丰乳肥臀》 爱人回家送奶奶 Memos: AWS 美东可用区 P0 故障(us-east-1) 2025 北京社保下限上调|个体户缴费随之上涨 铁锅重生记 不锈钢盆与放心水源改造计划 阅读《不被大风吹倒》 基于 Supabase 构建示例应用(中篇):实现 Vue 前端页面 阅读《三体》之地球往事 Oracle Free 实例重装系统 非京籍个体户缴纳社保(补充):“无有效的汇总预处理信息” 解决办法 阅读《芯片简史》 阅读《简约至上:交互式设计四策略(第2版)》 阅读《审判》 阅读《统计数字会撒谎》 达达秒送骑士 日本关西系列|在动物园前站找到海南本线 观影《长安的荔枝》 Memos: 记录两个在线工具 地球 Online:外卖骑手体验报告 杜师傅夜话:附身与归途 日本关西系列|使用投放硬币的行李寄存箱 日本关西系列|将多余零钱充值到西瓜卡 日本关西系列|网上购买大阪往返白滨高速巴士 乌鲁木齐・赛里木湖 “778 老哥” 摄影摘选(转载) 使用 Restic 来备份重要数据 Backing Up Important Data with Restic Sauvegarder des données importantes avec Restic Resticで重要なデータをバックアップする Cursor 开发 Obsidian 插件记录 非京籍个体户缴纳社保(十):新增并缴纳个人所得税-工资薪金 非京籍个体户缴纳社保(九):公积金开户增员与缴费 非京籍个体户缴纳社保(八):税务申报与工商年报 非京籍个体户缴纳社保(七):社保费用申报与缴纳 非京籍个体户缴纳社保(六):医保公共服务平台 - 增员确认 非京籍个体户缴纳社保(五):北京电子税务局 - 税务报道 非京籍个体户缴纳社保(四):北京市社会保险网上服务平台 - 增员与社保卡领取 非京籍个体户缴纳社保(三):社会保险网上服务平台 - 单位信息登记 非京籍个体户缴纳社保(二):北京 e 窗通平台提交申请 非京籍个体户缴纳社保(一):概览与先期准备 养老保险零基础入门指南(速通版) 了解北京门诊看病工会“二次报销”互助金 注册 US.KG 免费域名(dpdns.org) 白嫖 Cloudflare R2 + Worker 搭建私有镜像仓库 再思 JWT 的使用场景和算法选择 黑龙江・木兰县属小村落的星空(2024) Nginx 启用 HTTPS/3 优化网站的 SSL Labs 总体评级为 A+(禁用旧协议 & 启用 HSTS) 了解 OCSP Stapling 证书吊销验证机制 山东・烟台中秋两三日(2024) 分享改造后的博客发布流程和访问链路 边缘网络:白嫖 Cloudflare R2 博客图床(DNS 国内外分流)
基于 Supabase 构建示例应用(上篇):数据库与接口
2025-08-23 · via 东东's Blog

目标:实现一个文章发布 Web 应用(Demo),包含用户注册、编辑、发布文章的功能,同时有广场(展示所有已发布文章、所有用户可以查看)、并使用管理员进行维护,技术架构是 Supabase + Vue,Vue 部署在哪里还未计划,以此为契机学习了解 Supabase 服务

本篇学习和记录了如下内容

  1. 体验 Supabase Console 建表、执行 SQL
  2. 创建 RLS 行级安全策略
  3. 了解不同的 API Key 类型、创建 API Key、Curl 命令调用接口
  4. 了解 Supabase 的 Users 和业务表的 Users 表关联方式(通过触发器)
  5. 模拟用户注册、登陆、创建文章、查看广场文章的接口使用场景
  6. Edge Functions 配置一些封控策略(未测试)

设计表

用户表

CREATE TABLE users (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  username TEXT UNIQUE CHECK (char_length(username) >= 3),
  email TEXT UNIQUE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

users 表通过 id 字段扩展了 Supabase 内置的auth.users

文章表

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  title VARCHAR(255) NOT NULL,
  content TEXT,
  published_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

users 表和 posts 表之间存在一对多的关系,即一个用户可以拥有多篇文章。

触发器

-- 首先创建一个函数来更新 updated_at 字段
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

-- 为 users 表创建触发器
CREATE TRIGGER update_users_updated_at 
    BEFORE UPDATE ON users 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

-- 为 posts 表创建触发器
CREATE TRIGGER update_posts_updated_at 
    BEFORE UPDATE ON posts 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

Supabase 建表

点击 SQL Editor 侧边栏,右侧输入框执行 SQL 语句,执行后来到 Table Editor,可以看到已经成功创建两张表

01.webp

但是上方都有 Unrestricted 标注,没有开启 RLS 策略,可以点击页面上的黄色 “RLS disabled” 按钮,或是运行以下 SQL 语句均可开启。

ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

启用行级策略后,我们可以设置一些策略,例如:

允许用户只能插入(INSERT)属于自己的文章

CREATE POLICY "用户只能发表自己的文章" 
ON posts 
FOR INSERT 
WITH CHECK (auth.uid() = user_id); 

这样,用户尝试插入文章时,Supabase 会检查要插入的 user_id 是否等于当前登录用户的 ID

备注:策略也可以在 Authentication 模块的 Policies 子模块中,点击表后进行可视化创建。

创建用户

Supabase 提供邮件邀请和手动创建的方式

02.webp

可以手动创建用户

也可以点击 “Send invitation” 发送邀请到邮箱。

获取免费的 SMPT Server(可选)

最开始我以为 Supabase 发送邮件需要自己提供服务,就在网上找了一个 Brevo 这个服务,有免费额度,注册过程很丝滑,记录如下,这步可以跳过,直接使用 Supabase 的邮件服务就好,也不用设置。

服务地址:Free SMTP Server | Deliver to the Inbox Every Time

免费套餐每天 300 封,测试使用足够了

04.webp

注册后点击右上角的组织,选择 “SMTP & API”

05.webp

可以看到 SMTP 服务的服务器地址、用户名密码

06.webp

还需要添加 Sender,否则发不出邮件

这里我起的应用名字是 “Reader Bot”,邮箱就是我的 Gmail 邮箱,需要真实的邮箱地址,稍后会发送验证码验证。

07.webp

点击添加

提示我们的免费邮箱可能大概率进入收件人的垃圾邮箱,建议绑定域名,因为是测试,当然是 Anyway

验证完成后,可以看到新的 Sender 添加成功

09.webp

此时 Brevo 提供的邮箱服务已可用

在 Supabase 配置自己的 Email SMPT 服务(可选)

如果你申请了邮箱,可以在 Authentication 模块进行配置

10.webp

填入 Brevo 获取到的邮箱服务信息

Sender email:k********0@gmail.com
Sender name:Reader Bot
Host:smtp-relay.brevo.com
Port number:587
Username:954923002@smtp-brevo.com
Password:(YOUR-SMTP key value)

此时,再回到 Authentication 下的 Users 表中发送邮件邀请用户,就可以收到邮件

11.webp

内容如下

其中的邮件内容模板在 “Emails - Templates” 内进行配置,访问的链接在 “URL Configuration - Site URL” 进行配置,暂时先不用修改

初识 API & Keys

在 Project Settings 的 API Keys 页面可以创建项目 Key

13.webp

创建后有一个 Publishable key 可以在浏览器使用,格外注意,需要搭配 RLS 策略使用,它是可以公开的

sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj

页面下方还可以看到一个名为 default 的 Server Key 用于服务端机器、或者 Function、Workers 等

点击页面左侧的 API Docs 来到 API 页面

这里有一个知识点:Supabase 通过原生集成的 PostgREST,为数据库提供开箱即用的 Auto API,使开发者无需部署后端即可安全地进行基础的 CRUD 操作。

API 文档很细致,API 分为 Client API 和 Server API,Clinet API 可以

通过 API 注册登陆读写表数据

以下命令的 apikey 就是刚生成的 Publishable key(测试时可以使用临时邮箱, e.g. TEMP MAIL

curl -X POST 'https://iurlblwfhmulfdysyqdz.supabase.co/auth/v1/signup' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Content-Type: application/json" \
-d '{
  "email": "nasovec941@chaublog.com",
  "password": "123456"
}'

返回(已格式化)

{  
    "id": "b0fec5be-879d-4912-863e-b7495a8906b9",  
    "aud": "authenticated",  
    "role": "authenticated",  
    "email": "nasovec941@chaublog.com",  
    "phone": "",  
    "confirmation_sent_at": "2025-08-23T07:19:42.840305897Z",  
    "app_metadata": {  
        "provider": "email",  
        "providers": [  
            "email"  
        ]  
    },  
    "user_metadata": {  
        "email": "nasovec941@chaublog.com",  
        "email_verified": false,  
        "phone_verified": false,  
        "sub": "b0fec5be-879d-4912-863e-b7495a8906b9"  
    },  
    "identities": [  
        {  
            "identity_id": "c9527fc9-13c9-4c85-a2c1-5b776663f22e",  
            "id": "b0fec5be-879d-4912-863e-b7495a8906b9",  
            "user_id": "b0fec5be-879d-4912-863e-b7495a8906b9",  
            "identity_data": {  
                "email": "nasovec941@chaublog.com",  
                "email_verified": false,  
                "phone_verified": false,  
                "sub": "b0fec5be-879d-4912-863e-b7495a8906b9"  
            },  
            "provider": "email",  
            "last_sign_in_at": "2025-08-23T07:19:42.817329844Z",  
            "created_at": "2025-08-23T07:19:42.81738Z",  
            "updated_at": "2025-08-23T07:19:42.81738Z",  
            "email": "nasovec941@chaublog.com"  
        }  
    ],  
    "created_at": "2025-08-23T07:19:42.772681Z",  
    "updated_at": "2025-08-23T07:19:44.000369Z",  
    "is_anonymous": false  
}

会收到 Supabase 注册邮件,目前地址会跳转到 localhost:3000 地址,我们的前端 Demo 还没开发部署(但是需要点击一下确认链接进行 Supabase 的用户激活)

此处仅体验使用 Publishable key 调用 Supabase 后端 API,注册用户后登陆:

curl -X POST 'https://iurlblwfhmulfdysyqdz.supabase.co/auth/v1/token?grant_type=password' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Content-Type: application/json" \
-d '{
  "email": "nasovec941@chaublog.com",
  "password": "123456"
}'

返回

{  
    "access_token": "eyJhbGaciOiJ...NULE72KZGcdu6-LfeUdSbp8",  
    "token_type": "bearer",  
    "expires_in": 3600,  
    "expires_at": 1755937882,  
    "refresh_token": "u4jw6puieja6",  
    "user": {  
        "id": "b0fec5be-879d-4912-863e-b7495a8906b9",  
        "aud": "authenticated",  
        "role": "authenticated",  
        "email": "nasovec941@chaublog.com",  
        "email_confirmed_at": "2025-08-23T07:21:22.503059Z",  
        "phone": "",  
        "confirmation_sent_at": "2025-08-23T07:19:42.840305Z",  
        "confirmed_at": "2025-08-23T07:21:22.503059Z",  
        "last_sign_in_at": "2025-08-23T07:31:22.263515322Z",  
        "app_metadata": {  
            "provider": "email",  
            "providers": [  
                "email"  
            ]  
        },  
        "user_metadata": {  
            "email": "nasovec941@chaublog.com",  
            "email_verified": true,  
            "phone_verified": false,  
            "sub": "b0fec5be-879d-4912-863e-b7495a8906b9"  
        },  
        "identities": [  
            {  
                "identity_id": "c9527fc9-13c9-4c85-a2c1-5b776663f22e",  
                "id": "b0fec5be-879d-4912-863e-b7495a8906b9",  
                "user_id": "b0fec5be-879d-4912-863e-b7495a8906b9",  
                "identity_data": {  
                    "email": "nasovec941@chaublog.com",  
                    "email_verified": true,  
                    "phone_verified": false,  
                    "sub": "b0fec5be-879d-4912-863e-b7495a8906b9"  
                },  
                "provider": "email",  
                "last_sign_in_at": "2025-08-23T07:19:42.817329Z",  
                "created_at": "2025-08-23T07:19:42.81738Z",  
                "updated_at": "2025-08-23T07:19:42.81738Z",  
                "email": "nasovec941@chaublog.com"  
            }  
        ],  
        "created_at": "2025-08-23T07:19:42.772681Z",  
        "updated_at": "2025-08-23T07:31:22.270377Z",  
        "is_anonymous": false  
    }  
}

以上都是 GETTING STARTED 的内容,接下来可以看下业务表相关的 API

14.webp

翻到 Insert 语句,“创建一篇文章”

curl -X POST 'https://iurlblwfhmulfdysyqdz.supabase.co/rest/v1/posts' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Authorization: Bearer eyJhbGaciOiJ...NULE72KZGcdu6-LfeUdSbp8" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d '{ "title": "你好,世界!", "content": "北京今日天气:22-29度 多云 西北风3级" }'

报错

{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"posts\""}%

RLS 行级策略不允许,执行以下策略:

-- 用户只能插入自己的帖子
CREATE POLICY "允许认证用户插入帖子"
ON posts
FOR INSERT
TO authenticated
WITH CHECK (true);

-- 用户只能更新自己的帖子
CREATE POLICY "用户只能更新自己的帖子"
ON posts
AS PERMISSIVE
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

再次调用,没有返回任何内容,查看 Table Editor 可以看到已经多了一条记录

15.webp

然后我发现 users 表也没有记录,即 Supabase 的 auth.users 和我的 public.users 表没有关联,同时 posts 表的 user_id 也是空;

分别解决这两个问题,对于 users 未同步,可以创建一个触发器

-- 创建函数
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.users (id, email, username)
  VALUES (
    NEW.id,
    NEW.email,
    COALESCE(NEW.raw_user_meta_data->>'username', SPLIT_PART(NEW.email, '@', 1))
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 创建触发器
CREATE OR REPLACE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

好了,再注册个用户试试!

用户:oyentreng@deepyinc.com 密码:123456

注册、登陆不再重复粘贴代码,控制台已经可以看到用户

16.webp

解决 posts 的 user_id 为空的问题可以创建如下触发器

-- 启用 uuid 扩展(如果尚未启用)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- 创建触发器函数
CREATE OR REPLACE FUNCTION public.set_post_user_id()
RETURNS TRIGGER AS $$
BEGIN
  -- 从 JWT 中获取用户 ID 并设置
  NEW.user_id = auth.uid();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 创建触发器
CREATE OR REPLACE TRIGGER set_post_user_id_trigger
  BEFORE INSERT ON posts
  FOR EACH ROW
  EXECUTE FUNCTION public.set_post_user_id();

使用新的用户 Token 创建文章

curl -X POST 'https://iurlblwfhmulfdysyqdz.supabase.co/rest/v1/posts' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Authorization: Bearer eyJhbGaciOiJ...YNGEpJM2MkFp0pj4Hj45RY-h9L06M4" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d '{ "title": "你好,世界!", "content": "北京今日天气:22-29度 多云 西北风3级(下起了小雨)" }'

解决了

17.webp

查看 “广场” 文章

以用户身份分页请求 10 篇文章

curl 'https://iurlblwfhmulfdysyqdz.supabase.co/rest/v1/posts?published_at=not.is.null&select=*' \
-H "apikey: sb_publishable_JToCFTBwo4urIOPnE9cIFg_1Wo-6WYj" \
-H "Authorization: Bearer eyJhbGaciOiJ...YNGEpJM2MkFp0pj4Hj45RY-h9L06M4" \
-H "Range: 0-9"

查询出来 [] 空数组,因为文章最开始我们定义了一个 “只能查询(SELECT)到已发布的文章” 的策略

现在正好试试 Server Key 的管理员 Key 的威力,批量更新 published_at 字段为当前时间(Server Key 要藏好)

curl -X PATCH 'https://iurlblwfhmulfdysyqdz.supabase.co/rest/v1/posts?id=in.(3,4,5)' \
-H "apikey: sb_secret_Xr48DdK*************pmR1XA_xLeM45LH" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d '{ "published_at": "now()" }'

apikey 设置为 sb_secret_xxxx 管理员密钥,无需 Authorization Header

18.webp

已更新,预期应该是可以查询到了,但是依然返回 [],到 Supabase 翻翻,发现还是 RLS 策略缺失的问题;

Supabase SQL 执行页面有个功能,可以选择作为哪个 ROLE 执行

19.webp

默认使用 postgres,也可以切换为匿名角色或是 authenticated role,选择后可以选择特定的用户,到表的 RLS policies 页面,添加策略

using 条件就是发布时间不为空,允许了匿名和已登陆用户查看;

[  
    {  
        "id": 3,  
        "user_id": null,  
        "title": "你好,世界!",  
        "content": "北京今日天气:22-29度 多云 西北风3级",  
        "published_at": "2025-08-23T08:38:44.481162+00:00",  
        "created_at": "2025-08-23T07:58:08.479927+00:00",  
        "updated_at": "2025-08-23T08:38:44.481162+00:00"  
    },  
    {  
        "id": 4,  
        "user_id": null,  
        "title": "你好,世界!",  
        "content": "北京今日天气:22-29度 多云 西北风3级(下起了小雨)",  
        "published_at": "2025-08-23T08:38:44.481162+00:00",  
        "created_at": "2025-08-23T08:18:26.565641+00:00",  
        "updated_at": "2025-08-23T08:38:44.481162+00:00"  
    },  
    {  
        "id": 5,  
        "user_id": "badab300-959f-42f7-ae75-99d74f937804",  
        "title": "你好,世界!",  
        "content": "北京今日天气:22-29度 多云 西北风3级(下起了小雨)",  
        "published_at": "2025-08-23T08:38:44.481162+00:00",  
        "created_at": "2025-08-23T08:25:13.986663+00:00",  
        "updated_at": "2025-08-23T08:38:44.481162+00:00"  
    }  
]

“广场” 功能没问题,通过接口查询到了所有已发布的文章;

最后增加些安全策略

RLS 策略:用户每天最多发表 10 篇文章

CREATE POLICY "用户每天只能插入10篇文章"
ON posts 
FOR INSERT 
TO authenticated
WITH CHECK (
  auth.uid() = user_id AND
  -- 可以添加其他限制条件,比如每天最多10篇
  (SELECT COUNT(*) FROM posts 
   WHERE user_id = auth.uid() 
   AND created_at > NOW() - INTERVAL '1 day') < 10
);

在 Edge Functions 可以配置函数,以下的限制借助边缘函数实现

21.webp

rate-limiter(30 请求每分钟)

import "jsr:@supabase/functions-js/edge-runtime.d.ts";

// 内存存储速率限制
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();

Deno.serve(async (req: Request) => {
  // 获取客户端IP
  const clientIP = req.headers.get('x-forwarded-for') || 'unknown';

  // 速率限制检查
  const now = Date.now();
  const limitData = rateLimitMap.get(clientIP) || { count: 0, resetTime: now + 60000 };

  // 重置计数器(每分钟)
  if (now > limitData.resetTime) {
    limitData.count = 0;
    limitData.resetTime = now + 60000;
  }

  // 检查限制(每分钟30次)
  if (limitData.count >= 30) {
    return new Response(
      JSON.stringify({ error: 'Rate limit exceeded' }),
      { status: 429 }
    );
  }

  // 增加计数
  limitData.count++;
  rateLimitMap.set(clientIP, limitData);

  // 返回成功响应
  return new Response(
    JSON.stringify({ 
      success: true,
      method: req.method,
      remaining: 30 - limitData.count 
    }),
    { headers: { 'Content-Type': 'application/json' } }
  );
});

post-content-size(限制 content 字段 10kb 大小)

import "jsr:@supabase/functions-js/edge-runtime.d.ts";

Deno.serve(async (req: Request) => {
  try {
    // 读取请求内容
    const content = await req.text();

    // 计算内容大小(字节数)
    const contentSize = new TextEncoder().encode(content).length;

    // 检查大小限制(10KB = 10240字节)
    if (contentSize > 10240) {
      return new Response(
        JSON.stringify({ 
          error: 'Content too large',
          max_size: '10KB',
          actual_size: `${(contentSize / 1024).toFixed(2)}KB`
        }),
        { status: 413 }
      );
    }

    // 内容大小合格
    return new Response(
      JSON.stringify({ 
        success: true,
        size: `${contentSize} bytes`,
        size_kb: `${(contentSize / 1024).toFixed(2)}KB`
      }),
      { headers: { 'Content-Type': 'application/json' } }
    );

  } catch (error) {
    return new Response(
      JSON.stringify({ error: 'Invalid request' }),
      { status: 400 }
    );
  }
});

reg-user-limiter(每天限制最多 100 人注册)

import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "jsr:@supabase/supabase-js@2";

const supabase = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);

Deno.serve(async (req: Request) => {
  try {
    // 查询今日注册数量
    const today = new Date().toISOString().split('T')[0];
    const { count, error } = await supabase
      .from('auth.users')
      .select('*', { count: 'exact', head: true })
      .gte('created_at', `${today}T00:00:00`)
      .lte('created_at', `${today}T23:59:59`);

    if (error) throw error;

    // 检查是否超过限制
    if (count >= 100) {
      return new Response(
        JSON.stringify({ 
          error: 'Daily registration limit reached',
          limit: 100,
          today_count: count
        }),
        { status: 429 }
      );
    }

    // 允许注册
    return new Response(
      JSON.stringify({ 
        allowed: true,
        remaining: 100 - count,
        today_count: count
      }),
      { headers: { 'Content-Type': 'application/json' } }
    );

  } catch (error) {
    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500 }
    );
  }
});

AI 提供的 Function 函数,我配置上,但没测试是否能正常工作 🤷

不知不觉记录了不少内容,后端目前先了解这些,已覆盖 Demo 所需功能,Web 前端的开发放到下篇文章进行记录。

下篇会根据 Supabase 的文档示例,使用 Vue 开发前端页面,也会调研部署在哪个免费服务比较好。