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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 00:42:42