diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 0000000..cc9f3a3 --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,274 @@ +package auth + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" +) + +// setupTestDB 初始化测试数据库 +func setupTestDB() { + // 使用 SQLite 内存模式 + cfg.Config.DB = "sqlite" + cfg.Config.DSN = fmt.Sprintf("file::memory:?cache=shared&_time=%d", time.Now().UnixNano()) + + // 初始化数据库表 + if err := models.AllModels.AutoMigrate(cfg.DB()); err != nil { + panic(err) + } +} + +// TestMain 负责测试环境的全局初始化 +func TestMain(m *testing.M) { + setupTestDB() + code := m.Run() + os.Exit(code) +} + +func getTestAuth() Auth { + // 每次获取一个新的实例名,避免测试间冲突(虽然内存库是共享的,但数据清理可能不完全) + // 为了简单起见,我们在 TestMain 中只初始化一次 DB,但可以通过应用名区分 + appKey := fmt.Sprintf("test_app_%d", time.Now().UnixNano()) + + a := Factory.New(appKey, models.AppConfig{ + Name: "Test App", + DefaultRoles: []models.RoleDefinition{ + { + Code: "admin", + Name: "Administrator", + Policies: []string{"*:*"}, + }, + { + Code: "editor", + Name: "Editor", + Policies: []string{ + "article:create", + "article:read", + "article:update", + }, + }, + { + Code: "viewer", + Name: "Viewer", + Policies: []string{ + "article:read", + }, + }, + { + Code: "deleter", + Name: "Deleter", + Policies: []string{ + "article:delete", + }, + }, + }, + }) + + // 初始化 + if err := a.(*appAuth).init(); err != nil { + panic(err) + } + + return a +} + +// TestBasicPermission 测试基础权限授予与检查 +func TestBasicPermission(t *testing.T) { + a := getTestAuth() + ctx := context.Background() + userID := "user_basic_001" + + // 初始状态应该无权限 + ok, err := a.CheckPermission(ctx, userID, "", "article:read", "") + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if ok { + t.Error("Expected no permission initially") + } + + // 授予 viewer 角色 + if err := a.GrantRole(ctx, userID, "", "viewer"); err != nil { + t.Fatalf("GrantRole failed: %v", err) + } + + // 应该有 article:read 权限 + ok, err = a.CheckPermission(ctx, userID, "", "article:read", "") + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if !ok { + t.Error("Expected article:read permission after granting viewer role") + } + + // 不应该有 article:create 权限 + ok, err = a.CheckPermission(ctx, userID, "", "article:create", "") + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if ok { + t.Error("Expected no article:create permission for viewer") + } +} + +// TestWildcardPermission 测试通配符权限 +func TestWildcardPermission(t *testing.T) { + a := getTestAuth() + ctx := context.Background() + userID := "user_admin_001" + + // 授予 admin 角色 (*:*) + if err := a.GrantRole(ctx, userID, "", "admin"); err != nil { + t.Fatalf("GrantRole failed: %v", err) + } + + // 应该拥有所有权限 + tests := []string{ + "article:read", + "article:delete", + "user:create", + "config:update", // system:config:update 会被解析为 system 应用的权限,而 admin 是 test_app 的 admin + } + + for _, perm := range tests { + ok, err := a.CheckPermission(ctx, userID, "", perm, "") + if err != nil { + t.Errorf("CheckPermission %s failed: %v", perm, err) + continue + } + if !ok { + t.Errorf("Expected permission %s for admin", perm) + } + } +} + +// TestStrictPermissionIsolation 验证权限严格隔离性 (用户请求场景) +// 验证:拥有 delete 权限的用户,是否能通过 read 权限检查 +func TestStrictPermissionIsolation(t *testing.T) { + a := getTestAuth() + ctx := context.Background() + userID := "user_deleter_001" + + // 授予 deleter 角色 (只包含 article:delete) + if err := a.GrantRole(ctx, userID, "", "deleter"); err != nil { + t.Fatalf("GrantRole failed: %v", err) + } + + // 1. 检查 article:delete (应该通过) + ok, err := a.CheckPermission(ctx, userID, "", "article:delete", "") + if err != nil { + t.Fatalf("CheckPermission error: %v", err) + } + if !ok { + t.Errorf("Expected user to have article:delete permission") + } + + // 2. 检查 article:read (应该失败) + // 关键验证点:delete 不包含 read + ok, err = a.CheckPermission(ctx, userID, "", "article:read", "") + if err != nil { + t.Fatalf("CheckPermission error: %v", err) + } + if ok { + t.Errorf("Strict isolation failed: User with 'article:delete' passed 'article:read' check") + } +} + +// TestOrgIsolation 测试多租户/组织隔离 +func TestOrgIsolation(t *testing.T) { + a := getTestAuth() + ctx := context.Background() + userID := "user_org_001" + orgA := "org_a" + orgB := "org_b" + + // 在 OrgA 中授予 editor 角色 + if err := a.GrantRole(ctx, userID, orgA, "editor"); err != nil { + t.Fatalf("GrantRole failed: %v", err) + } + + // 在 OrgA 上下文中检查 article:create (应该通过) + ok, err := a.CheckPermission(ctx, userID, orgA, "article:create", "") + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if !ok { + t.Error("Expected permission in OrgA") + } + + // 在 OrgB 上下文中检查 article:create (应该失败) + ok, err = a.CheckPermission(ctx, userID, orgB, "article:create", "") + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if ok { + t.Error("Expected no permission in OrgB (role was granted in OrgA)") + } + + // 在全局上下文中检查 (应该失败,因为角色绑定在 OrgA) + ok, err = a.CheckPermission(ctx, userID, "", "article:create", "") + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if ok { + t.Error("Expected no global permission") + } +} + +// TestResourcePermission 测试特定资源权限 +func TestResourcePermission(t *testing.T) { + a := getTestAuth() + ctx := context.Background() + userID := "user_res_001" + resID := "doc_123" + + // 需要先创建权限定义,因为 GrantResourcePerm 会检查权限是否存在 + permID := fmt.Sprintf("%s:doc:read", a.(*appAuth).appKey) + perm := models.Permission{ + ID: permID, + AppKey: a.(*appAuth).appKey, + Resource: "doc", + Action: "read", + Description: "Read Doc", + } + if err := cfg.DB().Create(&perm).Error; err != nil { + t.Fatalf("Failed to create permission: %v", err) + } + + // 初始无权限 + ok, err := a.CheckPermission(ctx, userID, "", "doc:read", resID) + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if ok { + t.Error("Expected no permission") + } + + // 授予对特定资源 doc_123 的 doc:read 权限 + if err := a.GrantResourcePerm(ctx, userID, "", "doc:read", resID); err != nil { + t.Fatalf("GrantResourcePerm failed: %v", err) + } + + // 检查 doc:read on doc_123 (应该通过) + ok, err = a.CheckPermission(ctx, userID, "", "doc:read", resID) + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if !ok { + t.Error("Expected permission on specific resource") + } + + // 检查 doc:read on doc_456 (应该失败) + ok, err = a.CheckPermission(ctx, userID, "", "doc:read", "doc_456") + if err != nil { + t.Fatalf("CheckPermission failed: %v", err) + } + if ok { + t.Error("Expected no permission on other resource") + } +} diff --git a/cfg/cfg.go b/cfg/cfg.go index 9f3db14..dca137b 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -86,6 +86,9 @@ type OAuthProvidersConfig struct { var Config = &Options{ DB: "mysql", DSN: "root:123456@tcp(127.0.0.1:3306)/vbase?charset=utf8&parseTime=True&loc=Local", + Redis: config.Redis{ + Addr: "memory", + }, SMS: defaultSMS(), JWT: JWTConfig{ Secret: "your-secret-key-change-in-production-min-32-characters", diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..a2f1449 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,204 @@ +# VBase 后端架构设计文档 + +## 1. 概览 + +VBase 是一个基于 Golang 的高性能后端基础框架,旨在提供一套标准化的用户管理、组织架构(多租户)、权限控制(RBAC)和 OAuth2 认证服务。项目采用分层架构设计,基于 `vigo` 框架构建,强调代码的可维护性、扩展性和安全性。 + +## 2. 技术栈 + +- **语言**: Golang 1.22+ +- **Web 框架**: [vigo](https://github.com/veypi/vigo) (基于洋葱模型的高性能框架) +- **ORM**: GORM (支持 MySQL, PostgreSQL, SQLite 等) +- **配置管理**: `cfg` 包 (支持环境变量、配置文件) +- **认证**: JWT (JSON Web Token) + OAuth2 +- **数据库**: 关系型数据库 (MySQL/PostgreSQL) + +## 3. 系统架构 + +项目遵循经典的**洋葱模型**和**分层架构**: + +``` +Request -> [Global Middlewares] -> [Router] -> [Group Middlewares] -> [Handler] -> [Service/Logic] -> [Model/DAO] -> Database + | +Response <- [Global After Middleware] <------------------------------------+ +``` + +### 3.1 目录结构 + +``` +/ +├── api/ # API 接口层 (路由定义、请求处理) +│ ├── auth/ # 认证相关接口 (登录、注册、刷新Token) +│ ├── oauth/ # OAuth2 Provider 接口 +│ ├── org/ # 组织/租户管理接口 +│ ├── user/ # 用户管理接口 +│ └── init.go # 路由聚合与全局中间件配置 +├── auth/ # 核心权限控制模块 (RBAC 实现) +├── cfg/ # 配置与基础设施 (DB, Redis, Log) +├── models/ # 数据模型定义 (GORM 结构体) +├── libs/ # 通用工具库 +├── cli/ # 命令行入口 +└── doc/ # 文档 +``` + +## 4. 核心模块设计 + +### 4.1 认证与授权 (Auth Module) + +Auth 模块是系统的核心安全组件,实现了基于角色的访问控制 (RBAC) 和资源级权限管理。 + +#### 4.1.1 核心概念 + +- **Permission (权限)**: 最小粒度的操作许可,格式严格遵循 `resource:action` 或 `app:resource:action`。 + - 示例: `user:read`, `org:create`, `oauth-client:delete` + - **约束**: 资源标识符 (`resource`) 只能包含字母、数字、下划线和连字符,**严禁包含冒号**,以避免解析歧义。 +- **Role (角色)**: 权限的集合。系统预设角色包括 `admin` (管理员) 和 `user` (普通用户)。 +- **UserRole (用户-角色关联)**: 用户在特定组织 (`OrgID`) 下拥有的角色。 +- **Policy (策略)**: 定义角色拥有的权限列表。 + +#### 4.1.2 权限验证机制 + +系统提供 `auth.Auth` 接口和 `VBaseAuth` 实例,支持多种验证方式: + +1. **基础权限**: `Perm("resource:action")` - 检查用户是否拥有指定权限。 +2. **资源所有者**: `PermWithOwner("resource:action", "owner_id_key")` - 如果用户是资源所有者则放行,否则检查权限。 +3. **特定资源**: `PermOnResource("resource:action", "resource_id_key")` - 检查用户对特定资源实例的权限。 +4. **组合权限**: `PermAny`, `PermAll` - 支持“任一”或“所有”权限的逻辑组合。 +5. **通配符支持**: 支持 `*` 通配符,如 `app:resource:*` 匹配该资源下的所有操作。 + +#### 4.1.3 中间件流程 + +1. **AuthMiddleware**: 解析 JWT Token,提取 UserID 和 OrgID,注入到请求上下文 (`vigo.X`)。 +2. **PermMiddleware**: 在业务 Handler 执行前拦截请求,调用 `CheckPermission` 进行鉴权。 + +### 4.2 用户体系 (User Module) + +- **User**: 核心用户实体,包含基本信息 (昵称、头像、邮箱等)。 +- **Identity**: 认证信息表,支持多种登录方式 (密码、OAuth、验证码等) 关联到同一用户。 +- **Session**: 用户会话管理,用于记录登录状态和刷新 Token。 + +### 4.3 组织架构 (Org Module) + +支持多租户模型的组织管理: + +- **Org**: 组织/团队实体。 +- **OrgMember**: 组织成员关系表,记录用户在组织内的角色 (`RoleIDs`)。 +- **Context 隔离**: 所有数据操作通过 `OrgID` 进行逻辑隔离 (在 `auth.AuthMiddleware` 中解析并注入)。 + +### 4.4 OAuth2 服务 (OAuth Module) + +实现了完整的 OAuth2 Provider 功能,支持第三方应用接入: + +- **Client**: 第三方应用注册信息 (ClientID, ClientSecret)。 +- **AuthorizationCode**: 授权码模式支持。 +- **Token**: Access Token 和 Refresh Token 管理。 +- **Flows**: 支持 Authorization Code, Client Credentials, Refresh Token 等标准流程。 + +## 5. 接口规范 + +### 5.1 请求处理 + +- **参数解析**: 利用 `vigo` 的泛型 Handler 机制,自动解析 Query, Path, Header, JSON Body 参数到结构体。 +- **验证**: 结构体 Tag (`src`, `json`, `default`) 定义参数源和默认值。 + +### 5.2 响应格式 + +统一使用 JSON 格式响应,由全局后置中间件 `common.JsonResponse` 处理: + +**成功响应**: +```json +{ + "code": 200, + "data": { ... } // 或 null +} +``` + +**错误响应**: +```json +{ + "code": 400, // 或 401, 403, 500 等 + "message": "错误描述信息" +} +``` + +## 6. 数据库设计 + +推荐使用 `vigo.Model` 作为基类,统一包含以下字段: +- `ID`: UUID (varchar(36)) +- `CreatedAt`: 创建时间 +- `UpdatedAt`: 更新时间 +- `DeletedAt`: 软删除标记 + +所有模型在 `models/init.go` 中注册,支持服务启动时自动迁移 (`AutoMigrate`)。 + +## 7. 部署与运维 + +- **配置**: 支持 `.env` 文件和环境变量覆盖。 +- **构建**: `go build -o vbase cli/main.go` +- **运行**: `./vbase -p 8080` + +## 8. 开发规范 + +1. **资源命名**: 权限资源标识符必须使用 `kebab-case` (如 `oauth-client`) 或 `snake_case`,**禁止使用冒号**。 +2. **错误处理**: 优先使用 `vigo.NewError` 或预定义错误 (如 `vigo.ErrNotFound`),以便统一处理 HTTP 状态码。 +3. **测试**: 编写集成测试脚本 (`test.sh`, `full_test.sh`) 覆盖核心业务流程。 + +## 9. 实战场景指南 (Use Cases) + +VBase 的 `Org` (组织) 设计非常灵活,既支持单租户应用,也支持复杂的多租户 B2B 系统。以下是几种典型场景的最佳实践。 + +### 场景一:B2C 电商平台 / 简单应用 (Single Tenant) + +**示例**: 网上书店、个人博客、C端工具应用。 +**特点**: 所有用户直接面对平台,不存在“团队”或“公司”的概念。 + +- **Org 使用策略**: + - **忽略 Org**: 绝大多数 API 请求不需要携带 `X-Org-ID`。 + - **权限管理**: 用户角色(如 `admin`, `user`)直接绑定到 Global 域 (`org_id=""`)。 + - **数据隔离**: 数据通常是全局可见(如商品列表)或基于 `UserID` 隔离(如用户订单)。 + +- **权限模型**: + - **管理员**: 拥有全局 `admin` 角色,管理所有书籍 (`book:create`, `book:delete`)。 + - **普通用户**: 拥有全局 `user` 角色,可以浏览和购买 (`book:read`, `order:create`)。 + - **接口调用**: 客户端无需处理 `OrgID`,直接使用 Token 访问。 + +### 场景二:B2B SaaS 系统 (Multi-Tenant) + +**示例**: 项目管理工具 (Jira)、企业协作平台 (Slack)、HR 系统。 +**特点**: 客户以“公司”或“团队”为单位,数据严格隔离。 + +- **Org 使用策略**: + - **强制 Org**: 几乎所有业务 API (如创建任务、查看员工) 都必须在 Header 中携带 `X-Org-ID`。 + - **数据隔离**: 数据库查询必须带上 `Where("org_id = ?", ctxOrgID)`。 + - **角色复用**: 同一个用户 (User) 可以加入多个组织 (Org),在不同组织中拥有不同角色 (在 A 公司是管理员,在 B 公司是普通成员)。 + +- **权限模型**: + - **用户张三**: + - 在 **字节跳动 (OrgA)**: 角色 `admin` -> 权限 `project:create`, `member:add`。 + - 在 **个人工作室 (OrgB)**: 角色 `viewer` -> 权限 `project:read`。 + - **鉴权流程**: `AuthMiddleware` 会自动校验张三是否是目标 Org 的成员,并将对应的角色权限加载到上下文中。 + +### 场景三:双边市场 / 平台型 SaaS (Hybrid) + +**示例**: 淘宝/亚马逊 (平台 + 商家)、外卖平台。 +**特点**: 既有平台运营方,又有独立商家 (Org),还有 C 端消费者。 + +- **Org 使用策略**: + - **商家端 (Seller)**: 类似 B2B 模式。商家登录后,操作店铺数据需携带 `X-Org-ID` (店铺ID)。 + - **消费者端 (Buyer)**: 类似 B2C 模式。消费者浏览商品不需要 Org 上下文,或者处于一个特殊的“公域”上下文中。 + - **平台管理 (Platform Admin)**: 拥有全局超级权限,可以管理所有 Org。 + +- **权限模型**: + - **平台管理员**: 全局 `super_admin`,可封禁任何商家 (`org:delete`)。 + - **商家**: 在自己的 Org 内拥有 `shop_admin`,管理自家商品 (`product:create`)。 + - **消费者**: 全局 `buyer`,可跨 Org 下单 (`order:create`)。 + +### 总结 + +| 场景 | Org 角色 | 权限绑定 | 请求 Header | 数据隔离方式 | +| :--- | :--- | :--- | :--- | :--- | +| **B2C (书店)** | 不使用 / 仅后台用 | 全局绑定 (`org_id=""`) | 无需 `X-Org-ID` | 基于 `UserID` 或公开 | +| **B2B (Jira)** | 核心隔离单元 | 绑定到特定 Org | **必须** `X-Org-ID` | 严格基于 `OrgID` | +| **平台 (淘宝)** | 商家店铺 | 商家绑定 Org / 买家全局 | 商家端需要 / 买家端不需要 | 商家数据基于 `OrgID` | + +在 VBase 中,通过 `CheckPermission` 接口的逻辑(优先检查指定 Org 的角色,若无则检查全局角色),可以完美兼容上述所有模式。