Files
leaudit-platform-frontend/auth_doc/user_permissions_api_design.md
T
2025-12-05 00:09:32 +08:00

7.2 KiB
Raw Blame History

用户权限API设计方案

1. 后端需要返回的数据结构

方案A:返回权限键数组(推荐)

接口: GET /api/v3/users/{user_id}/permissions 或集成在登录响应中

响应示例:

{
  "code": 0,
  "msg": "成功",
  "data": {
    "user_id": 123,
    "role_id": 5,
    "role_key": "provincial_admin",
    "role_name": "省级管理员",
    "permissions": [
      "prompt_template:list:read",
      "prompt_template:detail:read",
      "prompt_template:create:write",
      "prompt_template:update:write",
      "prompt_template:delete:delete",
      "document:list:read",
      "document:create:write"
    ]
  }
}

方案B:返回权限对象数组(更详细)

{
  "code": 0,
  "msg": "成功",
  "data": {
    "user_id": 123,
    "role_id": 5,
    "role_key": "provincial_admin",
    "role_name": "省级管理员",
    "permissions": [
      {
        "permission_key": "prompt_template:create:write",
        "module": "prompt_template",
        "resource": "create",
        "action": "write",
        "display_name": "创建提示词模板"
      },
      {
        "permission_key": "prompt_template:update:write",
        "module": "prompt_template",
        "resource": "update",
        "action": "write",
        "display_name": "更新提示词模板"
      }
    ]
  }
}

2. 后端SQL查询示例

-- 获取用户的所有权限(通过角色关联)
SELECT DISTINCT
    p.permission_key,
    p.module,
    p.resource,
    p.action,
    p.display_name
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN role_permissions rp ON ur.role_id = rp.role_id AND rp.grant_type = 'GRANT'
JOIN permissions p ON rp.permission_id = p.id
WHERE u.id = $1
  AND u.deleted_at IS NULL
  AND ur.deleted_at IS NULL;

3. 集成到现有认证流程

选项1:集成到JWT Token中

  • 优点:前端无需额外请求,直接从JWT解析
  • 缺点:JWT体积变大,权限变更需要重新登录
  • 适用场景:权限不经常变化,用户会话较短

选项2:集成到Session中

  • 优点:权限可以实时更新(每次loader都会调用getUserSession
  • 缺点:每次页面加载都要查询数据库
  • 适用场景:权限经常变化,需要实时控制

选项3:混合方案(推荐)

  • JWT中存储基础权限(permission_key数组)
  • 重要操作时后端再次验证
  • 权限变更后强制用户重新登录或刷新token

4. 前端权限检查工具

创建权限Hook

// app/hooks/usePermission.ts
import { useRouteLoaderData } from "@remix-run/react";

export function usePermission() {
  const rootData = useRouteLoaderData("root") as {
    permissions?: string[];
    userRole: string;
  };

  const permissions = rootData?.permissions || [];

  /**
   * 检查是否有指定权限
   * @param permissionKey 权限键,如 "prompt_template:create:write"
   * @returns boolean
   */
  const hasPermission = (permissionKey: string): boolean => {
    return permissions.includes(permissionKey);
  };

  /**
   * 检查是否有指定模块的任意权限
   * @param module 模块名,如 "prompt_template"
   * @returns boolean
   */
  const hasModulePermission = (module: string): boolean => {
    return permissions.some(p => p.startsWith(`${module}:`));
  };

  /**
   * 检查是否有指定动作的权限
   * @param module 模块名
   * @param action 动作,如 "create", "update", "delete"
   * @returns boolean
   */
  const hasActionPermission = (module: string, action: string): boolean => {
    return permissions.some(p =>
      p.startsWith(`${module}:`) && p.includes(`:${action}`)
    );
  };

  /**
   * 批量检查权限(需要全部满足)
   * @param permissionKeys 权限键数组
   * @returns boolean
   */
  const hasAllPermissions = (permissionKeys: string[]): boolean => {
    return permissionKeys.every(key => permissions.includes(key));
  };

  /**
   * 批量检查权限(满足任意一个即可)
   * @param permissionKeys 权限键数组
   * @returns boolean
   */
  const hasAnyPermission = (permissionKeys: string[]): boolean => {
    return permissionKeys.some(key => permissions.includes(key));
  };

  return {
    permissions,
    hasPermission,
    hasModulePermission,
    hasActionPermission,
    hasAllPermissions,
    hasAnyPermission
  };
}

在组件中使用

// app/routes/prompts._index.tsx
import { usePermission } from "~/hooks/usePermission";

export default function PromptsIndex() {
  const { hasPermission, hasActionPermission } = usePermission();

  // 检查是否有创建权限
  const canCreate = hasPermission("prompt_template:create:write");

  // 检查是否有编辑权限
  const canEdit = hasPermission("prompt_template:update:write");

  // 检查是否有删除权限
  const canDelete = hasPermission("prompt_template:delete:delete");

  // 或者使用通用检查
  const canManage = hasActionPermission("prompt_template", "write");

  return (
    <div>
      {canCreate && (
        <Button onClick={() => navigate("/prompts/new")}>
          新增提示词模板
        </Button>
      )}

      {/* 表格操作列 */}
      {canEdit && <button onClick={handleEdit}>编辑</button>}
      {canDelete && <button onClick={handleDelete}>删除</button>}
    </div>
  );
}

5. 后端验证(双重保险)

前端权限检查只是为了改善用户体验,后端必须再次验证权限

// 后端API示例(Node.js/Express风格)
app.post('/api/v3/prompt-templates', authenticate, async (req, res) => {
  const userId = req.user.id;

  // 验证用户是否有创建权限
  const hasPermission = await checkUserPermission(
    userId,
    'prompt_template:create:write'
  );

  if (!hasPermission) {
    return res.status(403).json({
      code: 403,
      msg: '权限不足:您没有创建提示词模板的权限'
    });
  }

  // 执行创建逻辑...
});

6. 实施步骤

第一步:数据库准备

-- 确保用户角色关联表存在
-- user_roles: user_id <-> role_id

-- 确保角色权限关联表存在
-- role_permissions: role_id <-> permission_id (已存在)

-- 为现有用户分配角色和权限

第二步:后端API开发

  1. 创建 GET /api/v3/users/current/permissions 接口
  2. 在登录响应中包含权限列表
  3. 在JWT payload中包含permissions字段(可选)

第三步:前端集成

  1. 修改 app/api/login/auth.server.ts,在getUserSession中获取权限
  2. 修改 app/root.tsx loader,将permissions传递给前端
  3. 创建 app/hooks/usePermission.ts
  4. 修改 app/routes/prompts._index.tsx 使用权限检查

第四步:测试验证

  1. 测试不同角色的用户看到的功能是否正确
  2. 测试后端API权限验证是否生效
  3. 测试权限变更后是否需要重新登录

7. 注意事项

  1. 安全性:前端权限检查只是UI控制,不能替代后端验证
  2. 性能:权限列表应该缓存,避免每次请求都查询数据库
  3. 同步性:权限变更后,考虑强制用户重新登录或刷新token
  4. 降级方案:如果获取权限失败,应该有合理的降级策略(如只读模式)
  5. 审计日志:所有权限相关的操作应该记录审计日志