# 鉴权与授权模块设计 (Authentication & Authorization) `auth` 模块为 VBase 应用提供了灵活且强大的权限控制系统。它摒弃了隐式的“所有权猜测”,转而采用**显式授权**(ACL)和**手动鉴权**相结合的策略,以确保系统的安全性和可维护性。 ## 1. 核心鉴权机制概览 我们提供三种层级的鉴权方式,分别适用于不同的业务场景: | 方式 | 描述 | 适用场景 | 示例 | | :--- | :--- | :--- | :--- | | **全局中间件** (`Perm`) | 检查用户是否拥有某种**全局能力**(基于角色)。 | 管理后台、列表查询、无需特定资源ID的操作。 | `user:read` (查看所有用户) | | **简单手动鉴权** (Manual) | 在业务代码中直接检查资源字段(如 `OwnerID`)。 | **高频/私有资源**(如订单、日志),逻辑简单且追求高性能。 | 修改自己的订单 | | **复杂资源鉴权** (`PermOnResource`) | 基于数据库 ACL 表检查用户对**特定实例**的权限。 | **高价值/共享资源**(如项目、组织),需要精细控制和跨用户授权。 | 修改项目成员 | --- ## 2. 基础:上下文与中间件 ### 2.1 认证中间件 (AuthMiddleware) 认证中间件仅负责验证 JWT Token 的有效性,并将核心的用户 ID 注入到请求上下文中。为了保持高性能,它**不会**预加载用户详情、角色或组织信息。 ### 2.2 上下文访问 请使用 `auth` 包提供的辅助函数来访问上下文中的认证信息: ```go func MyHandler(x *vigo.X) { // 1. 获取当前用户 ID (核心,所有认证请求都可用) userID := auth.GetUserID(x) if userID == "" { // 未登录 return vigo.ErrUnauthorized } // 2. 获取当前组织 ID // 注意:仅当路由使用了 LoadOrg 中间件,或手动调用了 LoadOrg 后才可用 orgID := auth.GetOrgID(x) // 3. 获取在当前组织中的角色列表 // 注意:仅当路由使用了 LoadOrg 中间件后可用 roles := auth.GetOrgRoles(x) } ``` ### 2.3 组织上下文加载 (LoadOrg) 对于需要组织上下文的接口(如 `/api/orgs/{org_id}/...`),使用 `LoadOrg` 中间件按需加载组织信息。 - **行为**: 自动从 Path (`{org_id}`), Query (`?org_id=`), Header (`X-Org-ID`) 中解析组织 ID。 - **验证**: 检查当前用户是否为该组织的有效成员。 - **注入**: 将 OrgID 和用户在该组织的角色注入上下文。 ```go // 路由注册示例 // 对于需要组织上下文的接口,显式添加 LoadOrg 中间件 Router.Get("/{org_id}", "获取组织详情", auth.VBaseAuth.LoadOrg, auth.VBaseAuth.Perm("org:read"), get) ``` --- ## 3. 场景一:全局/类别权限 (Perm 中间件) 用于控制“谁能进入这个门”。不关心具体操作哪个数据,只关心你有没有这个资格。 - **原理**: 检查用户的角色(Role)是否包含指定权限。 - **配置**: 通常在路由定义时使用。 ```go // 只有管理员 (拥有 user:delete 权限) 可以删除用户 Router.Delete("/{user_id}", auth.VBaseAuth.Perm("user:delete"), DeleteUser) // 只有拥有 user:read 权限的人可以查看列表 Router.Get("/", auth.VBaseAuth.Perm("user:read"), ListUsers) ``` --- ## 4. 场景二:简单手动鉴权 (Manual Check) 对于大多数业务场景(如电商订单、个人博客文章),资源的所有权非常明确且单一。此时,直接在业务代码中判断 `UserID` 字段是最简单、最高效的方式。 **推荐范式:** ```go // 更新文章 (PATCH /articles/{id}) func UpdateArticle(x *vigo.X) { // 1. 获取 ID articleID := x.PathParams.Get("id") currentUserID := auth.GetUserID(x) // 2. 查库获取资源 article := db.GetArticle(articleID) // 3. 【核心鉴权逻辑】 // 允许条件:(我是作者) OR (我是管理员/有特权) if article.OwnerID != currentUserID { // 如果不是作者,再检查是否有全局权限进行兜底 if !auth.VBaseAuth.CheckPerm(x.Context(), currentUserID, "", "article:update", "") { return vigo.ErrForbidden } } // 4. 执行更新 db.Save(article) } ``` **优点**: - **零开销**: 不需要额外的 ACL 表查询。 - **逻辑清晰**: 鉴权逻辑与业务逻辑紧密结合,不易出错。 --- ## 5. 场景三:复杂资源鉴权 (PermOnResource 中间件) 对于像“项目协作”、“共享文档”这样需要多人协作、权限动态分配的资源,简单的字段判断就不够用了。这时我们需要引入 ACL (Access Control List)。 ### 5.1 使用方法 **路由配置**: 告诉中间件去检查 `user_permissions` 表,看当前用户对这个 `project_id` 是否有 `project:update` 权限。 ```go // 这里的 "project_id" 是 URL 路径参数的 key Router.Patch("/{project_id}", auth.VBaseAuth.PermOnResource("project:update", "project_id"), UpdateProject) ``` ### 5.2 授权 (Grant) 在创建资源时,必须**显式**授予创建者权限。 ```go func CreateProject(x *vigo.X) { // ... 创建项目逻辑 ... project := db.CreateProject(...) // 【关键】赋予创建者对该资源的全部权限 // 权限ID "project:*" 表示对该项目的所有操作权限 auth.VBaseAuth.GrantResourcePerm(x.Context(), currentUserID, orgID, "project:*", project.ID) } ``` --- ## 6. 设计决策记录 (ADR) ### 6.1 废弃隐式所有权推断 (PermWithOwner) 我们移除了曾尝试自动推断资源所有权的 `PermWithOwner` 中间件。因为在中间件层无法准确知道资源的 `OwnerID` 字段名,也无法处理复杂的“多所有者”场景,这导致了安全漏洞和难以调试的错误。现在我们推荐使用 **Manual Check** 模式。 ### 6.2 上下文精简 为了减少内存占用和数据库压力,Request Context (`vigo.X`) 中现在**仅**包含最核心的 `UserID`。其他的非核心信息(如组织信息、详细的用户资料)都改为**按需加载**(On-Demand)或通过特定中间件(如 `LoadOrg`)加载。 ### 6.3 组织信息解耦 组织信息的获取逻辑已从核心 `AuthMiddleware` 中剥离,移至 `LoadOrg` 中间件。这意味着不涉及组织业务的接口(如个人资料、系统设置)不再承担加载组织信息的额外开销。