add docs and auth test

v3
veypi 1 week ago
parent dea82e80a1
commit f42d36f71f

@ -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")
}
}

@ -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",

@ -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 的角色,若无则检查全局角色),可以完美兼容上述所有模式。
Loading…
Cancel
Save