Next.js 14 + Supabase角色权限实现:存储与验证方案咨询
针对Next.js 14 + Supabase角色权限管理的方案解答
你的思路是完全可行的,这也是Next.js + Supabase栈下实现角色权限管理的常规落地路径,下面结合Supabase的特性给你补充细节和优化建议:
1. 独立角色表的必要性:必须建
Supabase自带的auth.users表是系统核心表,不建议直接修改或添加自定义字段(避免版本更新时出现兼容性问题)。推荐设计两张独立表来管理角色关系:
-- 存储角色基础信息(比如admin、editor、user等) CREATE TABLE roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(50) UNIQUE NOT NULL, description TEXT ); -- 关联用户与角色(支持单用户多角色) CREATE TABLE user_roles ( user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, role_id UUID REFERENCES roles(id) ON DELETE CASCADE, PRIMARY KEY (user_id, role_id) ); -- 给认证用户授权访问这两张表的权限 GRANT SELECT, INSERT, UPDATE, DELETE ON roles, user_roles TO authenticated;
2. JWT嵌入角色:用Supabase触发器自动处理
你提到的"登录后取角色存JWT"的思路没问题,但不用手动实现,直接用Supabase的Postgres触发器自动将角色注入JWT的app_metadata字段,这样OAuth登录后会自动携带角色信息:
CREATE OR REPLACE FUNCTION public.handle_user_role_jwt() RETURNS TRIGGER AS $$ BEGIN -- 新用户默认分配普通用户角色(需提前在roles表插入'user'角色) INSERT INTO public.user_roles (user_id, role_id) VALUES (NEW.id, (SELECT id FROM public.roles WHERE name = 'user')) ON CONFLICT DO NOTHING; -- 将用户角色写入auth.users的app_metadata字段,自动同步到JWT UPDATE auth.users SET app_metadata = jsonb_set(app_metadata, '{roles}', ( SELECT jsonb_agg(r.name) FROM public.user_roles ur JOIN public.roles r ON ur.role_id = r.id WHERE ur.user_id = NEW.id )) WHERE id = NEW.id; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- 绑定触发器到用户创建事件(包括OAuth登录自动创建的用户) CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_user_role_jwt();
3. Next.js 14 Middleware的角色校验实现
在middleware.ts中通过Supabase的createMiddlewareClient获取会话,直接从JWT的app_metadata里提取角色做路由级校验:
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'; import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export async function middleware(req: NextRequest) { const res = NextResponse.next(); const supabase = createMiddlewareClient({ req, res }); const { data: { session } } = await supabase.auth.getSession(); // 保护/admin路由,仅允许admin角色访问 if (req.nextUrl.pathname.startsWith('/admin')) { if (!session) { return NextResponse.redirect(new URL('/login', req.url)); } const userRoles = session.user.app_metadata?.roles as string[] || []; if (!userRoles.includes('admin')) { return NextResponse.redirect(new URL('/unauthorized', req.url)); } } // 保护/dashboard路由,允许admin或editor角色访问 if (req.nextUrl.pathname.startsWith('/dashboard')) { if (!session) { return NextResponse.redirect(new URL('/login', req.url)); } const userRoles = session.user.app_metadata?.roles as string[] || []; if (!userRoles.some(role => ['admin', 'editor'].includes(role))) { return NextResponse.redirect(new URL('/unauthorized', req.url)); } } return res; } // 指定需要校验的路由规则 export const config = { matcher: ['/admin/:path*', '/dashboard/:path*'], };
4. 关键安全补充:结合Supabase RLS做数据层面校验
不要仅依赖前端/中间件校验,必须在Supabase的行级安全(RLS)策略中加入角色校验,防止绕过前端直接访问数据:
-- 示例:仅admin角色能查看所有用户数据 CREATE POLICY "Admin can view all users" ON auth.users FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.user_roles ur JOIN public.roles r ON ur.role_id = r.id WHERE ur.user_id = auth.uid() AND r.name = 'admin' ) ); -- 示例:仅自己或admin能修改用户信息 CREATE POLICY "User or admin can update profile" ON public.profiles FOR UPDATE USING ( auth.uid() = user_id OR EXISTS ( SELECT 1 FROM public.user_roles ur JOIN public.roles r ON ur.role_id = r.id WHERE ur.user_id = auth.uid() AND r.name = 'admin' ) );
内容的提问来源于stack exchange,提问作者shim0501
相关产品推荐
相关产品推荐

