You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Supabase为带API Key认证的公开REST API做数据库?

解决方案:自定义持久化API Key + Supabase行级安全(RLS)适配

核心思路

放弃用Supabase原生用户JWT做API认证,转而构建独立的API Key系统,在Express后端完成API Key验证后,将租户身份传递给Supabase,让RLS规则自动做数据隔离。既实现了持久化API Key的需求,又充分利用Supabase的RLS特性。


1. 第一步:在Supabase创建API Key存储表

先在Supabase控制台建一个api_keys表,用来存储租户的API Key(必须哈希后存储,禁止明文):

CREATE TABLE api_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id), -- 关联你的租户表,需提前创建tenants表
  key_hash TEXT NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  last_used_at TIMESTAMP WITH TIME ZONE,
  is_active BOOLEAN DEFAULT TRUE
);

-- 开启RLS,仅允许服务角色读写(避免前端直接访问该表)
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Service role manages API keys" ON api_keys FOR ALL USING (auth.role() = 'service_role');

后端生成API Key时用bcrypt哈希:

import bcrypt from 'bcrypt';
import crypto from 'crypto';

// 生成新API Key逻辑
const rawApiKey = crypto.randomBytes(32).toString('hex'); // 生成32位随机API Key
const keyHash = await bcrypt.hash(rawApiKey, 10);
// 将keyHash和对应tenant_id存入api_keys表

2. 第二步:Express中间件验证API Key

写一个Express中间件,从请求头提取API Key,验证有效性并获取租户ID:

import { Request, Response, NextFunction } from 'express';
import { createClient } from '@supabase/supabase-js';
import bcrypt from 'bcrypt';

const supabaseService = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export const validateApiKey = async (req: Request, res: Response, next: NextFunction) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid API Key' });
  }

  const rawApiKey = authHeader.split(' ')[1];
  // 查询所有活跃的API Key记录
  const { data: apiKeys, error } = await supabaseService
    .from('api_keys')
    .select('id, tenant_id, key_hash, is_active')
    .eq('is_active', true);

  if (error || !apiKeys.length) {
    return res.status(401).json({ error: 'Invalid API Key' });
  }

  // 逐个对比哈希值(bcrypt无法反向查询)
  let validKey = null;
  for (const key of apiKeys) {
    const isMatch = await bcrypt.compare(rawApiKey, key.key_hash);
    if (isMatch) {
      validKey = key;
      break;
    }
  }

  if (!validKey) {
    return res.status(401).json({ error: 'Invalid API Key' });
  }

  // 更新最后使用时间
  await supabaseService
    .from('api_keys')
    .update({ last_used_at: new Date() })
    .eq('id', validKey.id);

  // 将租户ID挂载到请求对象,供后续使用
  (req as any).tenantId = validKey.tenant_id;
  next();
};

3. 第三步:传递租户身份给Supabase,适配RLS

有两种可靠方式将租户ID传递给Supabase,让RLS规则自动生效:

方案A:自定义签发租户JWT

用Supabase的JWT密钥,签发包含tenant_id的长期JWT(有效期可设为7天或按需调整):

import jwt from 'jsonwebtoken';
import { createClient } from '@supabase/supabase-js';

// 验证API Key后生成租户JWT
const tenantJwt = jwt.sign(
  { tenant_id: (req as any).tenantId },
  process.env.SUPABASE_JWT_SECRET!,
  { expiresIn: '7d' }
);

// 创建带租户JWT的Supabase客户端
const supabaseTenant = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!,
  { global: { headers: { Authorization: `Bearer ${tenantJwt}` } } }
);

// 后续查询用该客户端,RLS自动生效
const { data: purchases } = await supabaseTenant.from('purchases').select('*');

配置purchases表的RLS规则:

ALTER TABLE purchases ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Tenant access own purchases" ON purchases FOR ALL USING (
  tenant_id = auth.jwt() ->> 'tenant_id'
);

方案B:服务角色客户端+租户上下文

如果不想签发JWT,可通过存储过程设置会话级租户上下文,RLS规则读取该上下文:

// 验证API Key后设置租户上下文
await supabaseService.rpc('set_tenant_context', { tenant_id: (req as any).tenantId });

// 用服务角色客户端查询,RLS自动生效
const { data: purchases } = await supabaseService.from('purchases').select('*');

先创建set_tenant_context存储过程:

CREATE OR REPLACE FUNCTION set_tenant_context(tenant_id TEXT)
RETURNS VOID AS $$
BEGIN
  PERFORM set_config('app.tenant_id', tenant_id, TRUE); -- TRUE表示会话级生效
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

配置purchases表的RLS规则:

ALTER TABLE purchases ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Tenant access own purchases" ON purchases FOR ALL USING (
  tenant_id = current_setting('app.tenant_id')::TEXT
);

方案优势

  • 无需手动在每个查询添加WHERE tenant_id = ...,RLS自动处理权限,避免漏写导致的安全漏洞
  • API Key持久化支持长期使用,同时通过哈希存储、状态开关、使用时间监控保证安全性
  • 权限逻辑集中在数据库层,后端代码更简洁,完全利用Supabase的RLS特性

内容的提问来源于stack exchange,提问作者Angelica

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 09:46:34