refactor(auth): 重构权限系统,简化API并添加缓存支持

主要变更:
- 将权限域从 appKey 改为 scope,权限ID格式为 scope:resource:action
- 新增 AddRole(roleCode, roleName, policies...) 方法,支持动态添加角色
- 简化 Factory.New() 只保留 scope 参数,移除 models.AppConfig 依赖
- PermAny/PermAll 改为变长参数 ...string 形式,使用更简洁
- AuthMiddleware 添加 Redis 缓存组织成员身份和角色信息(5分钟过期)
- 移除 models.AppConfig 和 models.RoleDefinition 结构体
- 更新测试和文档

BREAKING CHANGE: Factory.New() 签名变更,需要使用新的 AddRole API
v3
veypi 1 week ago
parent 37acea3420
commit 33eabfa013

@ -32,10 +32,15 @@ type Auth interface {
PermOnResource(permissionID, resourceKey string) func(*vigo.X) error
// 满足任一权限
PermAny(permissionIDs []string) func(*vigo.X) error
PermAny(permissionIDs ...string) func(*vigo.X) error
// 满足所有权限
PermAll(permissionIDs []string) func(*vigo.X) error
PermAll(permissionIDs ...string) func(*vigo.X) error
// ========== 角色管理 ==========
// 添加角色定义
// policies 格式: "resource:action",例如 "user:read", "*:*"
AddRole(roleCode, roleName string, policies ...string) error
// ========== 权限管理 ==========
// 授予角色
@ -71,43 +76,46 @@ var Factory = &authFactory{
// VBaseAuth vbase 自身的权限管理实例
// 由 vbase 包在初始化时注入
var (
VBaseAuth = Factory.New("vb", models.AppConfig{
Name: "VBase",
Description: "VBase 基础设施",
DefaultRoles: []models.RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"*:*"}},
{Code: "user", Name: "普通用户", Policies: []string{
"user:read", "user:update",
"org:read", "org:create",
"oauth-client:read", "oauth-client:create", "oauth-client:update", "oauth-client:delete",
}},
},
})
)
var VBaseAuth = Factory.New("vb")
func init() {
// 为 VBaseAuth 添加默认角色
VBaseAuth.AddRole("admin", "管理员", "*:*")
VBaseAuth.AddRole("user", "普通用户",
"user:read",
"user:update",
"org:read",
"org:create",
"oauth-client:read",
"oauth-client:create",
"oauth-client:update",
"oauth-client:delete",
)
}
type authFactory struct {
apps map[string]*appAuth // appKey -> auth实例
}
// New 创建权限管理实例(注册应用)
func (f *authFactory) New(appKey string, config models.AppConfig) Auth {
if _, exists := f.apps[appKey]; exists {
return f.apps[appKey]
}
// 验证默认角色中的权限格式
for _, role := range config.DefaultRoles {
for _, policy := range role.Policies {
validatePermissionID(policy)
}
// New 创建权限管理实例(注册权限域)
func (f *authFactory) New(scope string) Auth {
if _, exists := f.apps[scope]; exists {
return f.apps[scope]
}
auth := &appAuth{
appKey: appKey,
config: config,
}
f.apps[appKey] = auth
scope: scope,
roleDefs: make(map[string]roleDefinition),
policies: make(map[string][][2]string),
roleInitDone: make(map[string]bool),
}
// 设置权限域信息
auth.roleDefs["_scope_info"] = roleDefinition{
code: "_scope_info",
name: scope,
description: scope + " scope",
}
f.apps[scope] = auth
return auth
}
@ -145,10 +153,77 @@ func (f *authFactory) Init() error {
return nil
}
// appAuth 单个应用的权限管理
// roleDefinition 角色定义(内部使用)
type roleDefinition struct {
code string
name string
description string
policies []string // 权限列表: ["resource:action", "*:*"]
}
// appAuth 单个权限域的权限管理
type appAuth struct {
appKey string
config models.AppConfig
scope string // 权限域标识
roleDefs map[string]roleDefinition // roleCode -> role definition
policies map[string][][2]string // roleCode -> list of [resource, action] pairs
roleInitDone map[string]bool // roleCode -> whether role is initialized in DB
}
// AddRole 添加角色定义
// policies 格式: "resource:action",例如 "user:read", "*:*"
func (a *appAuth) AddRole(roleCode, roleName string, policies ...string) error {
if roleCode == "" || roleName == "" {
return fmt.Errorf("role code and name cannot be empty")
}
if roleCode == "_scope_info" {
return fmt.Errorf("reserved role code: _scope_info")
}
// 解析并验证权限格式
parsedPolicies := make([][2]string, 0, len(policies))
for _, policy := range policies {
// 严格检查格式: resource:action
parts := strings.Split(policy, ":")
if len(parts) != 2 {
return fmt.Errorf("invalid policy format: %s, expected 'resource:action'", policy)
}
resource, action := parts[0], parts[1]
// 验证 resource 和 action 不为空
if resource == "" || action == "" {
return fmt.Errorf("resource and action cannot be empty in policy: %s", policy)
}
// 验证 resource 格式(如果不是通配符)
if resource != "*" {
if !validResourceRegex.MatchString(resource) {
return fmt.Errorf("invalid resource identifier: %s in policy: %s, must start with letter and contain only letters, numbers, '-' or '_'", resource, policy)
}
}
// 验证 action 格式(如果不是通配符)
if action != "*" {
if !validResourceRegex.MatchString(action) {
return fmt.Errorf("invalid action identifier: %s in policy: %s, must start with letter and contain only letters, numbers, '-' or '_'", action, policy)
}
}
parsedPolicies = append(parsedPolicies, [2]string{resource, action})
}
// 存储角色定义
a.roleDefs[roleCode] = roleDefinition{
code: roleCode,
name: roleName,
}
a.policies[roleCode] = parsedPolicies
// 如果已经初始化过,立即同步到数据库
if len(a.roleInitDone) > 0 {
return a.initRole(roleCode)
}
return nil
}
// init 初始化应用的权限配置
@ -166,9 +241,12 @@ func (a *appAuth) init() error {
}
}
// 2. 创建系统预设角色
for _, roleDef := range a.config.DefaultRoles {
if err := a.initRole(roleDef); err != nil {
// 2. 创建系统预设角色(跳过 _app_info
for roleCode := range a.roleDefs {
if roleCode == "_app_info" {
continue
}
if err := a.initRole(roleCode); err != nil {
return err
}
}
@ -180,24 +258,25 @@ func (a *appAuth) init() error {
func (a *appAuth) extractPermissions() []models.Permission {
permMap := make(map[string]models.Permission)
for _, roleDef := range a.config.DefaultRoles {
for _, policy := range roleDef.Policies {
// policy 格式: "resource:action" 或 "*:*"
parts := strings.Split(policy, ":")
if len(parts) != 2 {
for roleCode, policies := range a.policies {
if roleCode == "_app_info" {
continue
}
resource, action := parts[0], parts[1]
permID := fmt.Sprintf("%s:%s:%s", a.appKey, resource, action)
for _, policy := range policies {
resource, action := policy[0], policy[1]
// 跳过通配符权限的特殊处理
if resource == "*" && action == "*" {
continue
}
permID := fmt.Sprintf("%s:%s:%s", a.scope, resource, action)
if _, exists := permMap[permID]; !exists {
permMap[permID] = models.Permission{
ID: permID,
AppKey: a.appKey,
Scope: a.scope,
Resource: resource,
Action: action,
Description: fmt.Sprintf("%s %s on %s", a.config.Name, action, resource),
Description: fmt.Sprintf("%s %s on %s", a.scope, action, resource),
}
}
}
@ -211,34 +290,44 @@ func (a *appAuth) extractPermissions() []models.Permission {
}
// initRole 初始化系统预设角色
func (a *appAuth) initRole(roleDef models.RoleDefinition) error {
func (a *appAuth) initRole(roleCode string) error {
roleDef, exists := a.roleDefs[roleCode]
if !exists {
return fmt.Errorf("role not found: %s", roleCode)
}
policies, hasPolicies := a.policies[roleCode]
if !hasPolicies {
policies = [][2]string{}
}
// 查找或创建系统角色
var role models.Role
err := cfg.DB().Where("code = ? AND org_id IS NULL", roleDef.Code).First(&role).Error
err := cfg.DB().Where("code = ? AND org_id IS NULL", roleDef.code).First(&role).Error
if err != nil {
// 创建新角色
role = models.Role{
OrgID: nil,
Code: roleDef.Code,
Name: roleDef.Name,
Description: roleDef.Description,
Code: roleDef.code,
Name: roleDef.name,
Description: roleDef.description,
IsSystem: true,
Status: 1,
}
if err := cfg.DB().Create(&role).Error; err != nil {
return fmt.Errorf("failed to create role %s: %w", roleDef.Code, err)
return fmt.Errorf("failed to create role %s: %w", roleDef.code, err)
}
}
// 同步角色权限
for _, policy := range roleDef.Policies {
parts := strings.Split(policy, ":")
if len(parts) != 2 {
hasWildcard := false
for _, policy := range policies {
resource, action := policy[0], policy[1]
// 处理通配符权限
if resource == "*" && action == "*" {
hasWildcard = true
continue
}
resource, action := parts[0], parts[1]
permID := fmt.Sprintf("%s:%s:%s", a.appKey, resource, action)
permID := fmt.Sprintf("%s:%s:%s", a.scope, resource, action)
// 检查关联是否存在
var count int64
@ -258,6 +347,26 @@ func (a *appAuth) initRole(roleDef models.RoleDefinition) error {
}
}
// 为通配符权限创建特殊记录
if hasWildcard {
wildcardPermID := fmt.Sprintf("%s:*:*", a.scope)
var count int64
cfg.DB().Model(&models.RolePermission{}).
Where("role_id = ? AND permission_id = ?", role.ID, wildcardPermID).
Count(&count)
if count == 0 {
rp := models.RolePermission{
RoleID: role.ID,
PermissionID: wildcardPermID,
Condition: "none",
}
if err := cfg.DB().Create(&rp).Error; err != nil {
return fmt.Errorf("failed to create wildcard role permission: %w", err)
}
}
}
a.roleInitDone[roleCode] = true
return nil
}
@ -366,7 +475,7 @@ func (a *appAuth) checkPermission(ctx context.Context, userID, orgID, permission
return nil
}
func (a *appAuth) PermAny(permissionIDs []string) func(*vigo.X) error {
func (a *appAuth) PermAny(permissionIDs ...string) func(*vigo.X) error {
for _, pid := range permissionIDs {
validatePermissionID(pid)
}
@ -396,7 +505,7 @@ func (a *appAuth) PermAny(permissionIDs []string) func(*vigo.X) error {
}
}
func (a *appAuth) PermAll(permissionIDs []string) func(*vigo.X) error {
func (a *appAuth) PermAll(permissionIDs ...string) func(*vigo.X) error {
for _, pid := range permissionIDs {
validatePermissionID(pid)
}
@ -511,7 +620,7 @@ func (a *appAuth) RevokeRole(ctx context.Context, userID, orgID, roleCode string
func (a *appAuth) GrantResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error {
if strings.Count(permissionID, ":") == 1 {
permissionID = fmt.Sprintf("%s:%s", a.appKey, permissionID)
permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID)
}
// 检查权限是否存在
var perm models.Permission
@ -557,7 +666,7 @@ func (a *appAuth) GrantResourcePerm(ctx context.Context, userID, orgID, permissi
func (a *appAuth) RevokeResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error {
if strings.Count(permissionID, ":") == 1 {
permissionID = fmt.Sprintf("%s:%s", a.appKey, permissionID)
permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID)
}
query := cfg.DB().Where("user_id = ? AND permission_id = ? AND resource_id = ?",
userID, permissionID, resourceID)
@ -604,7 +713,7 @@ func (a *appAuth) RevokeAll(ctx context.Context, userID, orgID string) error {
func (a *appAuth) CheckPermission(ctx context.Context, userID, orgID, permissionID, resourceID string) (bool, error) {
if strings.Count(permissionID, ":") == 1 {
permissionID = fmt.Sprintf("%s:%s", a.appKey, permissionID)
permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID)
}
// Check cache
@ -659,6 +768,12 @@ func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissi
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", parts[0], parts[1]))
// app:*:*
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", parts[0]))
} else if len(parts) == 2 {
// resource:action -> appKey:resource:action
fullPermID := fmt.Sprintf("%s:%s", a.scope, permissionID)
permsToCheck = append(permsToCheck, fullPermID)
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", a.scope, parts[0]))
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", a.scope))
}
// 检查这些角色是否有所需权限
@ -680,12 +795,24 @@ func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissi
if len(parts) == 3 {
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", parts[0], parts[1]))
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", parts[0]))
} else if len(parts) == 2 {
// resource:action -> appKey:resource:action
fullPermID := fmt.Sprintf("%s:%s", a.scope, permissionID)
permsToCheck = append(permsToCheck, fullPermID)
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", a.scope, parts[0]))
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", a.scope))
}
var userPermCount int64
query := cfg.DB().Model(&models.UserPermission{}).
Where("user_id = ? AND org_id = ? AND permission_id IN ? AND (expire_at IS NULL OR expire_at > ?)",
userID, orgID, permsToCheck, time.Now())
Where("user_id = ? AND permission_id IN ? AND (expire_at IS NULL OR expire_at > ?)",
userID, permsToCheck, time.Now())
if orgID != "" {
query = query.Where("org_id = ?", orgID)
} else {
query = query.Where("org_id IS NULL")
}
if resourceID != "" {
query = query.Where("resource_id = ? OR resource_id = '*'", resourceID)

@ -31,43 +31,21 @@ func TestMain(m *testing.M) {
}
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{
// 每次获取一个新的 scope避免测试间冲突虽然内存库是共享的但数据清理可能不完全
// 为了简单起见,我们在 TestMain 中只初始化一次 DB但可以通过 scope 区分
scope := fmt.Sprintf("test_scope_%d", time.Now().UnixNano())
a := Factory.New(scope)
// 添加角色定义
a.AddRole("admin", "Administrator", "*:*")
a.AddRole("editor", "Editor",
"article:create",
"article:read",
"article:update",
},
},
{
Code: "viewer",
Name: "Viewer",
Policies: []string{
"article:read",
},
},
{
Code: "deleter",
Name: "Deleter",
Policies: []string{
"article:delete",
},
},
},
})
)
a.AddRole("viewer", "Viewer", "article:read")
a.AddRole("deleter", "Deleter", "article:delete")
// 初始化
if err := a.(*appAuth).init(); err != nil {
@ -228,10 +206,10 @@ func TestResourcePermission(t *testing.T) {
resID := "doc_123"
// 需要先创建权限定义,因为 GrantResourcePerm 会检查权限是否存在
permID := fmt.Sprintf("%s:doc:read", a.(*appAuth).appKey)
permID := fmt.Sprintf("%s:doc:read", a.(*appAuth).scope)
perm := models.Permission{
ID: permID,
AppKey: a.(*appAuth).appKey,
Scope: a.(*appAuth).scope,
Resource: "doc",
Action: "read",
Description: "Read Doc",

@ -5,7 +5,10 @@
package auth
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/cache"
@ -14,9 +17,16 @@ import (
"github.com/veypi/vigo"
)
// orgMemberCache 组织成员身份缓存结构
type orgMemberCache struct {
IsMember bool `json:"is_member"`
RoleCodes []string `json:"role_codes"`
}
// AuthMiddleware 统一认证中间件
// 1. JWT认证: 解析token验证有效性设置用户信息
// 2. 组织上下文: 如果请求包含org_id验证用户成员身份设置组织信息
// 使用Redis缓存组织成员身份和角色信息减少数据库查询
func AuthMiddleware() func(*vigo.X) error {
return func(x *vigo.X) error {
// === 1. JWT 认证部分 ===
@ -59,21 +69,57 @@ func AuthMiddleware() func(*vigo.X) error {
return nil
}
// 验证用户是否为组织成员
var member models.OrgMember
if err := cfg.DB().Where("org_id = ? AND user_id = ? AND status = ?",
orgID, claims.UserID, models.MemberStatusActive).First(&member).Error; err != nil {
return vigo.ErrForbidden.WithString("you are not a member of this organization")
// 尝试从缓存获取组织成员信息
var roleCodes []string
var isMember bool
if cache.IsEnabled() {
cacheKey := fmt.Sprintf("auth:org_member:%s:%s", claims.UserID, orgID)
cachedData, err := cache.Get(cacheKey)
if err == nil && cachedData != "" {
var cached orgMemberCache
if err := json.Unmarshal([]byte(cachedData), &cached); err == nil {
isMember = cached.IsMember
roleCodes = cached.RoleCodes
}
}
}
x.Set("org_id", orgID)
// 缓存未命中,查询数据库
if roleCodes == nil {
// 验证用户是否为组织成员
var member models.OrgMember
err := cfg.DB().Where("org_id = ? AND user_id = ? AND status = ?",
orgID, claims.UserID, models.MemberStatusActive).First(&member).Error
isMember = err == nil
// 从 UserRole 表查询用户的角色
var roleCodes []string
if isMember {
// 查询用户的角色
cfg.DB().Model(&models.UserRole{}).
Joins("JOIN roles ON user_roles.role_id = roles.id").
Where("user_roles.user_id = ? AND user_roles.org_id = ?", claims.UserID, orgID).
Pluck("roles.code", &roleCodes)
}
// 写入缓存
if cache.IsEnabled() {
cacheData := orgMemberCache{
IsMember: isMember,
RoleCodes: roleCodes,
}
if data, err := json.Marshal(cacheData); err == nil {
cacheKey := fmt.Sprintf("auth:org_member:%s:%s", claims.UserID, orgID)
// 缓存5分钟
cache.Set(cacheKey, string(data), 5*time.Minute)
}
}
}
if !isMember {
return vigo.ErrForbidden.WithString("you are not a member of this organization")
}
x.Set("org_id", orgID)
x.Set("org_roles", roleCodes)
return nil

@ -77,29 +77,19 @@ VBase 提供了强大的 RBAC (基于角色的访问控制) 权限系统。
在您的应用中,使用 `vbase.Auth.New` 创建应用专属的权限实例。
```go
import (
"github.com/veypi/vbase"
"github.com/veypi/vbase/models"
)
import "github.com/veypi/vbase"
// 定义您的应用权限实例
var AppAuth = vbase.Auth.New("my_app", models.AppConfig{
Name: "My Application",
Description: "我的应用描述",
var AppAuth = vbase.Auth.New("my_app")
func init() {
// 定义应用的默认角色
DefaultRoles: []models.RoleDefinition{
{
Code: "admin",
Name: "管理员",
Policies: []string{"*:*"}, // 拥有所有权限
},
{
Code: "editor",
Name: "编辑",
Policies: []string{"article:create", "article:update"},
},
},
})
AppAuth.AddRole("admin", "管理员", "*:*") // 拥有所有权限
AppAuth.AddRole("editor", "编辑",
"article:create",
"article:update",
)
}
```
#### 2.2.2 使用权限中间件

@ -20,12 +20,12 @@ const (
)
// Permission 权限定义表(权限字典)
// ID 格式: app:resource:action (例如: crm:customer:read)
// ID 格式: scope:resource:action (例如: vb:user:read)
type Permission struct {
ID string `json:"id" gorm:"primaryKey;size:100" desc:"权限ID格式: app:resource:action"`
ID string `json:"id" gorm:"primaryKey;size:100" desc:"权限ID格式: scope:resource:action"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
AppKey string `json:"app_key" gorm:"index;size:50" desc:"应用标识"`
Scope string `json:"scope" gorm:"index;size:50" desc:"权限域标识"`
Resource string `json:"resource" gorm:"index;size:50" desc:"资源类型"`
Action string `json:"action" gorm:"index;size:50" desc:"操作类型"`
Description string `json:"description" desc:"权限描述"`
@ -107,21 +107,6 @@ func (UserPermission) TableName() string {
return "user_permissions"
}
// AppConfig 应用配置(用于权限初始化)
type AppConfig struct {
Name string `json:"name" desc:"应用名称"`
Description string `json:"description" desc:"应用描述"`
DefaultRoles []RoleDefinition `json:"default_roles" desc:"预设角色"`
}
// RoleDefinition 角色定义(配置用)
type RoleDefinition struct {
Code string `json:"code" desc:"角色代码"`
Name string `json:"name" desc:"角色名称"`
Description string `json:"description" desc:"角色描述"`
Policies []string `json:"policies" desc:"权限列表: ["customer:read", "*:*"]"`
}
// GrantRoleRequest 授予角色请求
type GrantRoleRequest struct {
UserID string `json:"user_id" desc:"用户ID"`

Loading…
Cancel
Save