You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/docs/auth.md

157 lines
6.2 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 鉴权与授权模块设计 (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` 中间件。这意味着不涉及组织业务的接口(如个人资料、系统设置)不再承担加载组织信息的额外开销。