185 lines
6.6 KiB
Markdown
185 lines
6.6 KiB
Markdown
# 11 角色、权限与路由整合策略
|
||
|
||
本节提供一套完整的“角色定义 → RLS 策略 → 前端路由/跳转 → 业务流程”整合方案,旨在将数据库安全模型与项目实际开发无缝结合。
|
||
|
||
---
|
||
|
||
## 1. 角色定义(权威口径)
|
||
|
||
为避免权限判断分裂,项目应统一使用 `public.ak_users.role` 作为唯一权威的角色字段。
|
||
|
||
### 1.1 推荐的角色枚举
|
||
|
||
- `customer`:消费者
|
||
- `merchant`:商家
|
||
- `delivery`:配送员
|
||
- `service`:客服
|
||
- `admin`:平台管理员
|
||
- `analytics`:数据分析/运营角色
|
||
|
||
> **决策点**:
|
||
> - `analytics` 角色是可选的。如果运营/分析师与 `admin` 权限边界不清,可以先统一为 `admin`。
|
||
> - 但长远看,为“数据查看者”设定独立角色有利于最小权限原则。
|
||
|
||
### 1.2 如何在数据库中获取当前用户角色
|
||
|
||
通常通过一个函数实现,该函数内部使用 `auth.uid()`。
|
||
|
||
```sql
|
||
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 鉴权模板**:
|
||
|
||
```sql
|
||
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`(或类似文件)中,应提供更精细的守卫函数。
|
||
|
||
**守卫函数建议**:
|
||
|
||
```typescript
|
||
// 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 页面中使用**:
|
||
|
||
```typescript
|
||
// pages/mall/analytics/index.uvue
|
||
onLoad(() => {
|
||
if (!ensureRole(['admin', 'analytics'], { toastTitle: '仅管理员可访问数据分析' })) {
|
||
return;
|
||
}
|
||
initDashboard();
|
||
});
|
||
```
|
||
|
||
### 3.3 业务流程闭环(以 Analytics 首页为例)
|
||
|
||
1. **用户访问** `/pages/mall/analytics/index`。
|
||
2. **前端守卫**:`onLoad` 中 `ensureRole(['admin', 'analytics'])` 执行:
|
||
- 未登录 → 跳转登录页
|
||
- 已登录但角色不符 → toast 提示 + 返回
|
||
3. **调用 Service**:`dashboardService.uts` 的 `fetch...` 函数被调用。
|
||
4. **执行 RPC**:`rpcOrNull('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` 子包页面中统一调用。
|