GoWind 开源生态GoWind 开源生态
首页
框架
GoWind Admin
GoWind CMS
GoWind IM
GoWind UBA
GoWind IoT
GoWind Toolkit
GoWind Quant
GitHub
首页
框架
GoWind Admin
GoWind CMS
GoWind IM
GoWind UBA
GoWind IoT
GoWind Toolkit
GoWind Quant
GitHub
  • 介绍

    • GoWind Admin 产品介绍
    • GoWind Admin 安装指南
  • 后端文档

    • 后端架构总览
    • 后端核心模块详解
    • 后端 API 与 Protobuf 定义
    • 后端配置与部署
    • 后端扩展开发
  • 前端文档

    • 前端架构总览
    • 前端核心功能详解
  • 二开教程

    • 后端新增业务模块实战教程
    • 前端新增业务页面实战教程
    • 前后端联调完整实战教程
  • 高级教程

    • Lua 脚本扩展实战教程
    • 权限系统深度解析教程
    • 多租户架构实战教程
    • 任务调度与异步处理教程
    • 文件上传与对象存储教程
    • 事件总线与解耦架构教程
    • 前端主题定制与国际化教程
    • 性能优化与监控教程
    • SSE 实时推送
    • 登录策略与安全加固
    • 加密工具实战

权限系统深度解析教程

权限系统是 GoWind Admin 的核心能力之一,支持 Casbin、OPA、Zanzibar 三种授权引擎,提供细粒度的访问控制。本教程深入讲解权限模型、配置方法和实战场景。

一、权限模型概览

1.1 三种授权引擎对比

特性CasbinOPA (Open Policy Agent)Zanzibar
类型ACL/RBAC/ABAC策略引擎(Rego 语言)关系型授权
复杂度⭐⭐ 简单⭐⭐⭐⭐ 复杂⭐⭐⭐ 中等
性能高(内存计算)中(需编译策略)高(图数据库)
学习曲线低高中
适用场景传统 RBAC复杂动态策略社交网络、协作平台
后端实现pkg/authorizer/casbinpkg/authorizer/opapkg/authorizer/zanzibar

1.2 权限维度

GoWind Admin 的权限体系包含三个维度:

维度说明示例
菜单权限控制用户能看到哪些菜单"文章管理"菜单对普通员工隐藏
接口权限控制用户能调用哪些 API只有管理员能删除用户
数据权限控制用户能访问哪些数据范围销售经理只能看本部门数据

二、RBAC 模型详解

2.1 核心概念

用户 (User) → 角色 (Role) → 权限 (Permission) → 资源 (Resource)
  • 用户:系统的使用者
  • 角色:权限的集合(如"超级管理员"、"编辑"、"访客")
  • 权限:对资源的操作许可(如"article:create"、"user:delete")
  • 资源:被保护的对象(如菜单、API 接口、数据行)

2.2 数据库表结构

-- 用户表
users (id, username, ...)

-- 角色表
roles (id, name, code, ...)

-- 权限表
permissions (id, name, code, resource_type, resource_id, ...)

-- 用户-角色关联
user_roles (user_id, role_id)

-- 角色-权限关联
role_permissions (role_id, permission_id)

-- 菜单表
menus (id, name, path, component, permission_code, ...)

2.3 权限代码规范

权限代码采用 资源:操作 的命名规范:

权限代码说明
article:list查看文章列表
article:create创建文章
article:update更新文章
article:delete删除文章
user:view查看用户信息
user:edit编辑用户
system:config系统配置

三、Casbin 引擎实战

Casbin 是一个国产开源访问控制库,支持 ACL/RBAC/ABAC 等模型,被 Intel、IBM、腾讯云、VMware 等公司使用。其核心由三个概念组成:

  • 请求(Request):由 (subject, object, action) 三元组组成,如 (bob, /users, GET)
  • 模型(Model):判定规则,定义了请求如何与策略匹配
  • 策略(Policy):用户与角色、资源、行为的映射关系

3.1 配置 Casbin

在 auth.yaml 中启用 Casbin:

authz:
  type: "casbin"
  
  casbin:
    model: |
      [request_definition]
      r = sub, obj, act
      
      [policy_definition]
      p = sub, obj, act
      
      [role_definition]
      g = _, _
      
      [policy_effect]
      e = some(where (p.eft == allow))
      
      [matchers]
      m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)

3.2 策略定义

Casbin 策略存储在数据库中(casbin_rule 表):

p, admin, /admin/v1/article/*, (GET|POST|PUT|DELETE)
p, editor, /admin/v1/article/*, (GET|POST|PUT)
p, viewer, /admin/v1/article/*, GET

g, alice, admin
g, bob, editor
g, charlie, viewer

解释:

  • p 行定义权限规则:主体、资源、操作
  • g 行定义角色继承:用户属于哪个角色

3.3 代码中使用

import "github.com/casbin/casbin/v2"

// 检查权限
func CheckPermission(enforcer *casbin.Enforcer, user string, resource string, action string) bool {
    allowed, err := enforcer.Enforce(user, resource, action)
    if err != nil {
        log.Errorf("check permission failed: %v", err)
        return false
    }
    return allowed
}

// 使用示例
if !CheckPermission(enforcer, "alice", "/admin/v1/article/123", "DELETE") {
    return errors.New("permission denied")
}

3.4 动态加载策略

当权限变更时,重新加载策略:

// 添加策略
enforcer.AddPolicy("editor", "/admin/v1/article/*", "DELETE")

// 删除策略
enforcer.RemovePolicy("viewer", "/admin/v1/article/*", "POST")

// 重新加载所有策略
enforcer.LoadPolicy()

四、OPA 引擎实战

4.1 配置 OPA

在 auth.yaml 中启用 OPA:

authz:
  type: "opa"
  
  opa:
    policy_path: "./policies"
    query: "data.authz.allow"

4.2 Rego 策略语言

创建策略文件 policies/authz.rego:

package authz

# 默认拒绝
default allow = false

# 管理员拥有所有权限
allow {
    input.user.roles[_] == "admin"
}

# 编辑可以创建和更新文章
allow {
    input.user.roles[_] == "editor"
    input.resource.type == "article"
    input.action in ["create", "update"]
}

# 访客只能查看
allow {
    input.user.roles[_] == "viewer"
    input.resource.type == "article"
    input.action == "view"
}

# 数据权限:用户只能查看自己的文章
allow {
    input.user.roles[_] == "author"
    input.resource.type == "article"
    input.action == "view"
    input.resource.author_id == input.user.id
}

4.3 请求上下文

OPA 接收的输入数据结构:

{
  "user": {
    "id": 123,
    "username": "alice",
    "roles": ["editor"]
  },
  "resource": {
    "type": "article",
    "id": 456,
    "author_id": 123
  },
  "action": "update"
}

4.4 代码中使用

import "github.com/open-policy-agent/opa/rego"

// 创建查询
query, err := rego.New(
    rego.Query("data.authz.allow"),
    rego.Input(input),
    rego.Load([]string{"./policies"}, nil),
).PrepareForEval(ctx)

// 执行评估
results, err := query.Eval(ctx)
if err != nil {
    return false, err
}

// 检查结果
allowed := len(results) > 0 && len(results[0].Expressions) > 0 && 
           results[0].Expressions[0].Value == true

五、Zanzibar 引擎实战

5.1 配置 Zanzibar

在 auth.yaml 中启用 Zanzibar(以 Keto 为例):

authz:
  type: "zanzibar"
  
  zanzibar:
    type: "keto"
    
    keto:
      write_url: "http://keto:4466"
      read_url: "http://keto:4466"
      use_grpc: true

5.2 关系元组

Zanzibar 基于关系元组(Relationship Tuples):

(user:Alice, is_member_of, group:Admins)
(group:Admins, can_access, resource:Dashboard)
(article:123, owned_by, user:Bob)

5.3 Keto 配置

创建 Keto 命名空间配置 namespaces.yaml:

namespaces:
  - name: user
    relations:
      member: {}
  
  - name: article
    relations:
      owner:
        types:
          - this
      viewer:
        types:
          - this
          - computedUserset:
              relation: owner
  
  - name: organization
    relations:
      member:
        types:
          - this
      admin:
        types:
          - this

5.4 代码中使用

import "github.com/ory/keto-client-go"

// 检查权限
func CheckAccess(ketoClient *keto.Client, subject string, relation string, object string) bool {
    resp, err := ketoClient.Check(context.Background(), &keto.CheckRequest{
        Namespace: "article",
        Object: object,
        Relation: relation,
        Subject: &keto.Subject{
            Id: subject,
        },
    })
    
    if err != nil {
        log.Errorf("check access failed: %v", err)
        return false
    }
    
    return resp.Allowed
}

// 使用示例
if !CheckAccess(ketoClient, "Alice", "viewer", "article:123") {
    return errors.New("access denied")
}

六、数据权限实现

6.1 数据权限范围

GoWind Admin 支持五种数据权限范围:

范围常量说明
仅本人SELF只能查看自己创建的数据
本部门UNIT_ONLY只能查看本部门的数据
本部门及下级UNIT_AND_CHILD查看本部门及所有下级部门的数据
指定部门SELECTED_UNITS查看指定部门的数据
全部ALL查看所有数据

6.2 实现原理

数据权限通过 Ent ORM 的 Viewer 机制实现:

// 创建用户级 Viewer
viewer := entgo.NewUserViewerContext(ctx, userID, dataScope)

// 在查询时自动应用过滤
articles, err := client.Article.Query().
    Where(article.HasAuthorWith(user.ID(userID))).
    All(viewer)

6.3 角色配置数据权限

在后台管理系统中:

  1. 进入 权限管理 → 角色管理
  2. 选择角色,点击"设置数据权限"
  3. 选择数据范围:
    • 如果选择"指定部门",需要勾选具体部门
  4. 保存

6.4 代码中获取数据权限

// 从 Context 获取当前用户的数据权限范围
dataScope := auth.GetDataScopeFromContext(ctx)

switch dataScope {
case identityV1.DataScope_SELF:
    query = query.Where(article.AuthorID(userID))
    
case identityV1.DataScope_UNIT_ONLY:
    query = query.Where(article.HasAuthorWith(
        user.HasOrgUnitsWith(orgunit.ID(orgUnitID)),
    ))
    
case identityV1.DataScope_ALL:
    // 不过滤
    
// ...
}

七、前端权限控制

7.1 路由级权限

根据用户权限动态注册路由:

// router/access.ts
import { useAccessStore } from '@vben/stores';

export function generateAccessRoutes(menus: Menu[]) {
  const routes: RouteRecordRaw[] = [];
  
  menus.forEach(menu => {
    if (menu.component) {
      routes.push({
        path: menu.path,
        component: () => import(`@/views/${menu.component}.vue`),
        meta: {
          title: menu.title,
          permission: menu.permission_code,
        },
      });
    }
  });
  
  return routes;
}

7.2 菜单级权限

根据权限过滤菜单显示:

// stores/modules/permission.ts
export const usePermissionStore = defineStore('permission', () => {
  const accessibleMenus = ref<Menu[]>([]);
  
  function setAccessibleMenus(menus: Menu[]) {
    // 过滤掉无权限的菜单
    accessibleMenus.value = filterMenusByPermission(menus);
  }
  
  return { accessibleMenus, setAccessibleMenus };
});

7.3 组件级权限

通过指令或组件控制按钮显示:

<script setup>
import { useAccess } from '@vben/access';

const { hasAccessByCodes } = useAccess();
</script>

<template>
  <!-- 方式 1:v-if 指令 -->
  <Button v-if="hasAccessByCodes(['article:create'])" type="primary">
    新建
  </Button>
  
  <!-- 方式 2:权限组件 -->
  <AccessControl codes="['article:delete']">
    <Button danger>删除</Button>
  </AccessControl>
</template>

7.4 权限 Composable

// composables/usePermission.ts
export function usePermission() {
  const accessStore = useAccessStore();
  
  function hasPermission(code: string): boolean {
    return accessStore.accessCodes.includes(code);
  }
  
  function hasAnyPermission(codes: string[]): boolean {
    return codes.some(code => hasPermission(code));
  }
  
  function hasAllPermissions(codes: string[]): boolean {
    return codes.every(code => hasPermission(code));
  }
  
  return { hasPermission, hasAnyPermission, hasAllPermissions };
}

八、权限审计日志

8.1 记录权限变更

每次权限变更都记录审计日志:

// 添加角色权限时记录日志
func (s *RoleService) AddPermissions(ctx context.Context, req *AddPermissionsRequest) error {
    // 执行添加操作
    err := s.repo.AddPermissions(ctx, req.RoleId, req.PermissionIds)
    if err != nil {
        return err
    }
    
    // 记录审计日志
    auditLogRepo.Create(ctx, &permissionV1.PermissionAuditLog{
        Action:      "add_permissions",
        RoleId:      req.RoleId,
        PermissionIds: req.PermissionIds,
        OperatorId:  auth.GetUserID(ctx),
        CreatedAt:   time.Now(),
    })
    
    return nil
}

8.2 查看权限审计日志

在后台管理系统中:

  1. 进入 日志管理 → 权限审计日志
  2. 可以看到所有权限变更记录:
    • 操作人
    • 操作时间
    • 操作类型(添加/移除权限)
    • 涉及的角色和权限

九、性能优化

9.1 缓存权限数据

将用户权限缓存到 Redis:

// 获取用户权限(带缓存)
func GetUserPermissions(ctx context.Context, userID uint32) ([]string, error) {
    cacheKey := fmt.Sprintf("user:%d:permissions", userID)
    
    // 尝试从缓存获取
    cached, err := redis.Get(ctx, cacheKey).Result()
    if err == nil {
        var permissions []string
        json.Unmarshal([]byte(cached), &permissions)
        return permissions, nil
    }
    
    // 从数据库查询
    permissions, err := queryPermissionsFromDB(userID)
    if err != nil {
        return nil, err
    }
    
    // 写入缓存(TTL 1 小时)
    data, _ := json.Marshal(permissions)
    redis.Set(ctx, cacheKey, data, time.Hour)
    
    return permissions, nil
}

9.2 批量权限检查

避免多次调用权限检查接口:

// ❌ 错误做法:循环检查
for _, resource := range resources {
    if !CheckPermission(user, resource, "view") {
        return errors.New("permission denied")
    }
}

// ✅ 正确做法:一次性获取所有权限
permissions := GetUserPermissions(user)
permissionSet := make(map[string]bool)
for _, p := range permissions {
    permissionSet[p] = true
}

for _, resource := range resources {
    if !permissionSet[resource] {
        return errors.New("permission denied")
    }
}

十、常见问题

Q1: 如何选择合适的授权引擎?

  • Casbin:适合传统的 RBAC 场景,配置简单,性能好
  • OPA:适合复杂的动态策略,如基于时间、地点、设备等多维度的权限判断
  • Zanzibar:适合社交网络、协作平台等需要复杂关系图谱的场景

Q2: 权限变更后何时生效?

  • 菜单权限:用户重新登录后生效(或前端主动刷新权限)
  • 接口权限:立即生效(Casbin 策略实时更新)
  • 数据权限:下次查询时生效

Q3: 如何实现权限继承?

通过角色继承实现:

超级管理员 → 管理员 → 编辑 → 访客

在 Casbin 中:

g, admin, super_admin
g, editor, admin
g, viewer, editor

Q4: 如何处理跨租户权限?

在多租户模式下,权限隔离通过以下方式实现:

  1. 租户级角色:每个租户有自己的角色和权限
  2. 数据隔离:查询时自动添加 tenant_id 过滤条件
  3. 跨租户访问:需要特殊权限代码(如 cross_tenant:view)

十一、相关文档

  • 后端核心模块详解
  • 前后端联调完整实战
  • Lua 脚本扩展实战
Edit this page
Last Updated:: 6/5/26, 11:29 AM
Contributors: Bobo
Prev
Lua 脚本扩展实战教程
Next
多租户架构实战教程