Files
medical-mall/docs/sql/11_roles_and_permissions_strategy.md

6.6 KiB
Raw Blame History

11 角色、权限与路由整合策略

本节提供一套完整的“角色定义 → RLS 策略 → 前端路由/跳转 → 业务流程”整合方案,旨在将数据库安全模型与项目实际开发无缝结合。


1. 角色定义(权威口径)

为避免权限判断分裂,项目应统一使用 public.ak_users.role 作为唯一权威的角色字段。

1.1 推荐的角色枚举

  • customer:消费者
  • merchant:商家
  • delivery:配送员
  • service:客服
  • admin:平台管理员
  • analytics:数据分析/运营角色

决策点

  • analytics 角色是可选的。如果运营/分析师与 admin 权限边界不清,可以先统一为 admin
  • 但长远看,为“数据查看者”设定独立角色有利于最小权限原则。

1.2 如何在数据库中获取当前用户角色

通常通过一个函数实现,该函数内部使用 auth.uid()

CREATE OR REPLACE FUNCTION public.get_current_user_role()
RETURNS TEXT
LANGUAGE sql
SECURITY DEFINER
AS $$
    SELECT role FROM public.ak_users WHERE auth_id = auth.uid() LIMIT 1;
$$;

2. RLS 策略与权限设计

2.1 权限分层(推荐)

  • A. Row Owner行归属者

    • 用户只能访问自己的数据,如地址、购物车、收藏、个人订单。
    • RLS 策略核心:auth.uid() = (SELECT auth_id FROM ak_users WHERE id = <row.user_id>)
  • B. Business Owner业务归属者

    • 商家只能访问自己店铺的数据,如商品、店铺订单。
    • RLS 策略核心:auth.uid() = (SELECT auth_id FROM ak_users WHERE id = <row.merchant_id>)
  • C. Privileged特权角色

    • admin/analytics 角色需要访问全局数据,尤其是聚合统计。
    • 强烈建议:不要为这些角色直接开放表的全局 SELECT 权限。

2.2 如何让 admin/analytics 安全地看全局数据?

推荐方案RPC + SECURITY DEFINER

  1. 维持表的严格 RLS:确保 customer/merchant 无法越权。
  2. Analytics 页面只调用 RPC:例如 rpc_analytics_* 系列函数。
  3. RPC 函数必须 SECURITY DEFINER:使其以“函数所有者”(通常是 postgres 超级用户)的权限执行,从而绕过调用者的 RLS 限制。
  4. RPC 函数内部必须做显式鉴权:这是安全闭环的关键。

RPC 鉴权模板

CREATE OR REPLACE FUNCTION public.rpc_analytics_sales_kpis(
    p_start_date DATE,
    p_end_date DATE
)
RETURNS TABLE (...)
LANGUAGE plpgsql
SECURITY DEFINER -- 以函数所有者权限执行
SET search_path = public -- 显式设置 search_path避免 search_path 攻击
AS $$
BEGIN
    -- 1. 在函数入口处做权限检查
    IF get_current_user_role() NOT IN ('admin', 'analytics') THEN
        RAISE EXCEPTION 'Permission denied: required role admin or analytics';
    END IF;

    -- 2. 执行统计(因为是 SECURITY DEFINER这里可以查到所有数据
    RETURN QUERY
    WITH ...
    -- ... 统计逻辑 ...
END;
$$;

现状风险:当前 rpc_analytics_* 脚本未包含 SECURITY DEFINER 与内部鉴权。若直接部署,当 RLS 开启时,admin/analytics 调用会因权限不足而查不到数据。


3. 前端项目整合:路由守卫与业务流程

3.1 路由分组(按角色)

项目页面按角色划分,便于集中管理路由与权限。

  • /pages/mall/consumer/**
  • /pages/mall/merchant/**
  • /pages/mall/delivery/**
  • /pages/mall/admin/**
  • /pages/mall/analytics/**

3.2 路由守卫(客户端鉴权)

services/analytics/authGuard.uts(或类似文件)中,应提供更精细的守卫函数。

守卫函数建议

// services/auth/guard.uts (示例)
import { getCurrentUser } from './user.uts' // 假设此函数能获取当前登录用户及其角色

// 1. 确保已登录
export function ensureLoggedIn(options: { redirect?: string } = {}): boolean {
  const user = getCurrentUser();
  if (!user) {
    uni.navigateTo({ url: options.redirect ?? '/pages/user/login' });
    return false;
  }
  return true;
}

// 2. 确保具备指定角色之一
export function ensureRole(allowedRoles: Array<string>, options: { toastTitle?: string } = {}): boolean {
  if (!ensureLoggedIn()) return false;

  const user = getCurrentUser();
  if (!user || !allowedRoles.includes(user.role)) {
    uni.showToast({ title: options.toastTitle ?? '无权访问', icon: 'none' });
    // 可选择返回上一页或跳转首页
    setTimeout(() => uni.navigateBack(), 1500);
    return false;
  }
  return true;
}

在 Analytics 页面中使用

// pages/mall/analytics/index.uvue
onLoad(() => {
  if (!ensureRole(['admin', 'analytics'], { toastTitle: '仅管理员可访问数据分析' })) {
    return;
  }
  initDashboard();
});

3.3 业务流程闭环(以 Analytics 首页为例)

  1. 用户访问 /pages/mall/analytics/index
  2. 前端守卫onLoadensureRole(['admin', 'analytics']) 执行:
    • 未登录 → 跳转登录页
    • 已登录但角色不符 → toast 提示 + 返回
  3. 调用 ServicedashboardService.utsfetch... 函数被调用。
  4. 执行 RPCrpcOrNull('rpc_analytics_sales_kpis', ...) 发起请求。
  5. 数据库鉴权rpc_analytics_sales_kpis 函数内部首先检查 get_current_user_role() 是否为 admin/analytics
    • 权限不足 → RAISE EXCEPTION,前端收到错误。
    • 权限通过 → 执行统计。
  6. 数据返回:前端拿到聚合数据并渲染。

这个流程实现了“前端快速失败 + 后端强制校验”的安全闭环。


4. 权限矩阵(总结)

角色 customer merchant admin/analytics
可读 上架商品、自己的(订单/地址/购物车/收藏/券) 自己的(商品/订单/店铺数据) 全局聚合数据(通过 RPC
可写 自己的(地址/购物车/收藏/订单创建) 自己的(商品/发货/售后) 通常不直接写业务表(通过后台管理功能)

5. 待办与实现建议

  1. 统一角色字段:在项目中明确 ak_users.role 为唯一权威,并提供获取当前用户角色的函数。
  2. 增强 RPC 安全性:为所有 rpc_analytics_* 函数增加 SECURITY DEFINER 与内部权限检查。
  3. 实现前端路由守卫:创建 ensureRole 函数,并在所有 analytics 子包页面中统一调用。