文章提醒,Supabase 服务端代码不能直接信任 getSession() 从 Cookie 读取的用户信息来决定访问权限。文中建议使用会校验访问令牌签名的 getClaims(),并检查 AI 生成代码及旧教程是否沿用了不安全的鉴权写法。
AI 使用声明:本文由我借助 AI 辅助撰写。我对照文末提到的 Supabase 和 Next.js 文档,核查了每一项论断和每一段代码示例。如果你发现错误,请在评论区告诉我,我会修正。
如果你让 AI Agent 为 Supabase + Next.js 应用编写身份验证代码,首先要检查一件事:服务端代码信任的是什么。
Agent(以及不少较早的教程)仍然会在 Server Components、Server Actions 和 route handlers 中生成这样的代码:
const { data: { session } } = await supabase.auth.getSession()
if (!session) redirect('/login')
const userId = session.user.id // used to decide what this request can read or write
这段代码看起来没问题,测试时也能正常运行。问题在于,这些数据来自哪里。
在服务端,getSession() 会从身份验证 cookie 中读取 session,但不会重新验证它。cookie 是用户输入。Supabase SSR 指南明确指出:任何人都可以伪造 session cookie,因此,绝不要在服务端代码中信任 getSession() 的结果。
如果 session.user.id 决定一次请求可以读写哪些行,那么伪造的 cookie 也就能决定这些权限。
getClaims() 会先验证 access token 的签名,再返回 claims:
对于使用非对称 JWT 签名密钥的项目(ECC 或 RSA,新项目默认采用这种方式),它会通过项目的 JWKS endpoint(/.well-known/jwks.json)验证签名。密钥通常会被缓存,因此验证速度很快。
对于仍使用对称密钥的项目,它会向 Auth server 发送请求来验证 token,与 getUser() 类似。
下面是一个受保护的 Server Component:
// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
export default async function DashboardPage() {
const supabase = await createClient()
const { data, error } = await supabase.auth.getClaims()
if (error || !data?.claims) redirect('/login')
const userId = data.claims.sub // verified subject (the user's id)
const { data: projects } = await supabase
.from('projects')
.select('id, name')
.eq('owner_id', userId) // keep RLS on as well; this filter is not your only lock
return <ProjectList projects={projects ?? []} />
}
即使 UI 中没有任何按钮调用某个 Server Action,它仍然可以通过 HTTP 被调用。要像对待 API route 一样对待每个 Server Action:验证输入、验证 claims,然后再写入数据。
如果你希望 Agent 不再生成旧的写法,免费的 Next.js + Supabase Loadout 规则包已经将这条要求设为固定规则。
标准修复说明页:Supabase SSR 中 getSession() 与 getClaims() 的区别。
Supabase SSR / 身份验证 cookie 指南(getSession 与经过验证的 claims)
Next.js App Router Server Components / Server Actions
如需进一步采取措施,你可以考虑屏蔽此人和/或举报滥用行为。