supabase是什么?
一个BaaS,在postgres上增加api层,为每个表生成api。并且支持 edge function(serverless函数调用),storage (对象存储),认证等
管理项目
创建一个Project,填写基本信息,包括数据库密码,选择Region
创建好之后,到首页可以看到一个URL和API KEY,前端将通过他来调用 supabase的提供的data api对数据库进行增删改查。
下面的小字说,这个key是可以被公开的,那么supabase是如何确保数据库操作的安全呢?答案是通过RLS实现的行级别的鉴权,这个后面会说到。
认证
supabase提供了开箱即用的认证功能,可以不写一行代码实现用户注册、登录等。
到 API docs -- User Managerment可以看到如何用去注册一个用户
我们命令行复制 并执行:
发现api成功返回了,然后到Table Editor中auth.users中可以看到,多了一条用户记录
这里schema有多种,其中auth是supabase直接管理的,用来实现认证,用户不能进行修改
用户创建的表在public下,创建完之后 supabase 会自动生成增删改查的api
建表
从上一部分可以看到,auth.users 并不存在 用户名称、简介等业务信息,就需要我们自己在public下创建一个users表,然后使用外键建立关联
设置外键,将public.users.uid 指向 auth.users.id ,并且设置当 auth.users记录被删除时,同步删除业务表的记录
trigger function
现在发现了个问题,当用户注册的时候,auth.users 会多一条记录,但是public.users却没有,难道还需要我们登录之后手动调用insert创建吗?
其实不用,我们可以使用supabase的trigger function,auth.users改变时触发,执行sql给public.users插入一条记录
到 Database -> function, 在public创建一个function,其实就是执行了一段sql,其中 new 代表auth.users中的一条记录,raw_user_meta_data 则是调用接口时传入的业务对象,这个现在看起来有点懵逼,后面会说到
-- 创建函数
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.users (uid, username)
VALUES (
NEW.id,
NEW.raw_user_meta_data->>'username',
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
有了执行逻辑,还要定义什么时候触发,到database -> triggers, 在public下新增一个trigger,指定到auth.users表插入的时候执行对应的trigger:
-- 创建触发器
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
然后删除刚刚的数据,
delete from auth.users where id = '06ca9840-a63f-447e-96af-e7f3ff768342'
重新注册一下,注意这里data对象就是上面函数中的raw_user_meta_data:
再查看一下业务表 public.users,多了一条记录
RLS: Row Level Security
如果想对某些行进行鉴权,比如
- 用户只能修改自己的信息
- 任何人都可以创建post
- 任何人只能查看自己的post
就需要用到RLS,通过创建polices来控制,官方的例子很具体了,这里解释一下
create policy "Public profiles are viewable only by authenticated users"
on profiles for select
to authenticated
using ( true );
表示创建了一个规则,对于profiles的select操作,对登录用户进行限制,限制为true,就是任何登录用户都可以查看profiles
create policy "Users can update their own profile."
on profiles for update
to authenticated -- the Postgres Role (recommended)
using ( (select auth.uid()) = user_id ) -- checks if the existing row complies with the policy expression
with check ( (select auth.uid()) = user_id ); -- checks if the new row complies with the policy expression
这里 auth.uid() 是从读取请求中的jwt解析得到的人当前登录用户的id,user_id是表中的列
这个polices限制了,对于profile的更新操作,只有登录用户,可以查看到自己的(using),并且修改后id不能变(with check),没有with check的话,用户可以把自己的profile中的uid改成别人的
这里区分一下using和with check的区别:
| 逻辑 | 含义 |
|---|---|
USING | 能不能选中这行(决定可操作范围) |
WITH CHECK | 改完后是否合法(防止越权写入) |
使用edge function实现限流
创建个业务表
再塞一条数据
由于没有设置ploices现在通过 api是请求不到的,增加一个polices,任何人都可以读取posts
postman中连续请求多次,都可以返回:
此时是没有限制速率的,supabase官方文档提供了通过redis实现限流的方案:supabase.com/docs/guides…
可以使用upstash serverless服务,创建好一个数据库可以到详情页面
可以在网页写限流的逻辑,但由于很多端点都需要用到限流,为了共用模块,我们使用supabase cli在本地进行开发,然后部署到云
在项目目录执行
supabase init
supabase login
supabase projects list # 查看项目列表
supabase link --project-ref <your-project-ref> # 绑定项目
目录结构整理如下:
➜ supabase_juejin tree -L 4
.
└── supabase
├── config.toml
└── functions
├── _shared
│ ├── auth.ts
│ └── ratelimit.ts
└── get-posts
├── deno.json
├── deno.lock
└── index.ts
由于auth和ratelimit可以各个函数都通用,我们把它放到_shared目录下,编写auth.ts如下
import { decode } from "https://deno.land/x/djwt@v2.8/mod.ts";
export function getToken(req: Request) {
const h = req.headers.get("Authorization") || "";
return h.startsWith("Bearer ") ? h.slice(7) : "";
}
export function getUserId(token: string): string {
try {
const decoded = decode(token) as [any, { sub?: string }, any];
return decoded[1].sub ?? "";
} catch {
return "";
}
}
export function getIp(req: Request) {
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
|| req.headers.get("cf-connecting-ip")
|| "unknown";
}
ratelimit.ts 如下:
import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts'
import { Ratelimit } from 'https://cdn.skypack.dev/@upstash/ratelimit@0.4.4'
import { getToken, getUserId, getIp } from './auth.ts';
// 限流配置接口
export interface RateLimitConfig {
requests: number;
window: string;
analytics?: boolean;
}
// 限流结果接口
export interface RateLimitResult {
success: boolean;
limit: number;
remaining: number;
reset: number;
identifier: string;
}
// 创建Redis连接
function createRedisClient(): Redis {
return new Redis({
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
});
}
// 创建限流器
function createRateLimiter(config: RateLimitConfig) {
const redis = createRedisClient();
return new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(config.requests, config.window),
analytics: config.analytics ?? true,
});
}
// 获取用户标识符
export function getIdentifier(req: Request): string {
const userId = getUserId(getToken(req));
const ip = getIp(req);
return userId || `ip:${ip}`;
}
// 检查限流
export async function checkRateLimit(
req: Request,
config: RateLimitConfig = { requests: 10, window: '1 m' }
): Promise<RateLimitResult> {
const identifier = getIdentifier(req);
const ratelimit = createRateLimiter(config);
const result = await ratelimit.limit(identifier);
return {
success: result.success,
limit: result.limit,
remaining: result.remaining,
reset: result.reset,
identifier
};
}
// 创建限流响应
export function createRateLimitResponse(result: RateLimitResult): Response {
const headers = {
'X-RateLimit-Limit': result.limit.toString(),
'X-RateLimit-Remaining': result.remaining.toString(),
'X-RateLimit-Reset': result.reset.toString()
};
if (!result.success) {
return new Response(JSON.stringify({
error: 'Rate limit exceeded',
limit: result.limit,
remaining: result.remaining,
reset: new Date(result.reset)
}), {
status: 429,
headers
});
}
return new Response(JSON.stringify({
success: result.success,
limit: result.limit,
remaining: result.remaining,
reset: new Date(result.reset),
identifier: result.identifier
}), {
status: 200,
headers
});
}
// 限流中间件 - 可以直接在Edge Function中使用
export async function withRateLimit(
req: Request,
handler: (req: Request) => Promise<Response>,
config: RateLimitConfig = { requests: 10, window: '1 m' }
): Promise<Response> {
try {
const rateLimitResult = await checkRateLimit(req, config);
if (!rateLimitResult.success) {
return createRateLimitResponse(rateLimitResult);
}
// 执行实际的处理器
const response = await handler(req);
// 添加限流头部到响应中
const headers = new Headers(response.headers);
headers.set('X-RateLimit-Limit', rateLimitResult.limit.toString());
headers.set('X-RateLimit-Remaining', rateLimitResult.remaining.toString());
headers.set('X-RateLimit-Reset', rateLimitResult.reset.toString());
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
} catch (error) {
return new Response(JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error'
}), {
status: 500,
});
}
}
get-post中可以先不调用接口,写点假数据 能调通就行:
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import { getUserId, getIp, getToken } from "@shared/auth.ts";
import { withRateLimit } from "@shared/ratelimit.ts";
console.log("Hello from Functions!")
// 实际的业务逻辑处理器
function getPostsHandler(req: Request): Response {
const token = getToken(req);
const userId = getUserId(token);
if (!userId) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
}
const ip = getIp(req);
const identifier = `${userId}-${ip}`;
const data = {
message: `Hello ${identifier}!`,
posts: [
{ id: 1, title: "Post 1", content: "This is post 1" },
{ id: 2, title: "Post 2", content: "This is post 2" }
]
};
return new Response(
JSON.stringify(data),
{ headers: { "Content-Type": "application/json" } }
);
}
// 使用限流中间件包装处理器
Deno.serve(async (req) => {
return await withRateLimit(
req,
(req) => Promise.resolve(getPostsHandler(req)),
{ requests: 5, window: '1 m' } // 每分钟最多5次请求
);
});
然后部署到云端:
supabase functions deploy
在命令行连续请求多次,发现可以触发限流的提示: