Skip to content
geelevelgeelevel

🛡️ 权限系统

Gin-Vue-Admin 采用 Casbin 实现基于角色的访问控制 (RBAC),提供灵活、强大的权限管理机制,支持多层级权限控制。

🎯 权限模型概述

RBAC 权限模型

用户 (User) ──┐
              ├─→ 角色 (Role) ──→ 权限 (Permission) ──→ 资源 (Resource)
用户组 (Group) ┘

权限层级结构

mermaid
graph TD
    A[超级管理员] --> B[系统管理员]
    A --> C[业务管理员]
    B --> D[普通用户]
    C --> D
    
    B --> E[用户管理权限]
    B --> F[系统配置权限]
    C --> G[业务数据权限]
    D --> H[基础查看权限]
    
    E --> I[API: /user/*]
    F --> J[API: /system/*]
    G --> K[API: /business/*]
    H --> L[API: /base/*]

🔧 Casbin 配置

模型配置文件

位置:server/resource/rbac_model.conf

ini
[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) && r.obj == p.obj && r.act == p.act

配置参数说明

配置项说明
request_definition请求定义:主体(sub)、对象(obj)、动作(act)
policy_definition策略定义:权限规则格式
role_definition角色定义:角色继承关系
policy_effect策略效果:允许访问的条件
matchers匹配器:权限验证逻辑

🛠️ 核心组件

Casbin 中间件

位置:server/middleware/casbin_rbac.go

go
// CasbinHandler Casbin权限验证中间件
func CasbinHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        claims, _ := c.Get("claims")
        waitUse := claims.(*utils.CustomClaims)
        
        // 获取请求信息
        obj := c.Request.URL.Path
        act := c.Request.Method
        sub := waitUse.AuthorityId
        
        // Casbin权限验证
        e := casbinService.Casbin()
        success, _ := e.Enforce(sub, obj, act)
        
        if !success {
            response.FailWithDetailed(gin.H{}, "权限不足", c)
            c.Abort()
            return
        }
        c.Next()
    }
}

Casbin 服务

位置:server/service/sys_casbin.go

go
type CasbinService struct{}

// UpdateCasbin 更新Casbin权限
func (casbinService *CasbinService) UpdateCasbin(authorityId string, casbinInfos []request.CasbinInfo) error {
    casbinService.ClearCasbin(0, authorityId)
    rules := [][]string{}
    for _, v := range casbinInfos {
        rules = append(rules, []string{authorityId, v.Path, v.Method})
    }
    e := casbinService.Casbin()
    success, _ := e.AddPolicies(rules)
    if !success {
        return errors.New("存在相同api,添加失败,请联系管理员")
    }
    return nil
}

// GetPolicyPathByAuthorityId 获取权限列表
func (casbinService *CasbinService) GetPolicyPathByAuthorityId(authorityId string) (pathMaps []request.CasbinInfo) {
    e := casbinService.Casbin()
    list := e.GetFilteredPolicy(0, authorityId)
    for _, v := range list {
        pathMaps = append(pathMaps, request.CasbinInfo{
            Path:   v[1],
            Method: v[2],
        })
    }
    return pathMaps
}

// ClearCasbin 清除权限
func (casbinService *CasbinService) ClearCasbin(v int, p ...string) bool {
    e := casbinService.Casbin()
    success, _ := e.RemoveFilteredPolicy(v, p...)
    return success
}

// Casbin 获取Casbin实例
func (casbinService *CasbinService) Casbin() *casbin.Enforcer {
    return global.GVA_CASBIN
}

🏗️ 权限数据结构

角色表 (sys_authorities)

go
type SysAuthority struct {
    CreatedAt     time.Time
    UpdatedAt     time.Time
    DeletedAt     *time.Time `sql:"index"`
    AuthorityId   string     `json:"authorityId" gorm:"not null;unique;primary_key;comment:角色ID;size:90"`
    AuthorityName string     `json:"authorityName" gorm:"comment:角色名"`
    ParentId      string     `json:"parentId" gorm:"comment:父角色ID"`
    DataAuthorityId []string `json:"dataAuthorityId" gorm:"-"`
    Children      []SysAuthority `json:"children" gorm:"-"`
    SysBaseMenus  []SysBaseMenu  `json:"menus" gorm:"many2many:sys_authority_menus;"`
    Users         []SysUser      `json:"-" gorm:"many2many:sys_user_authority;"`
    DefaultRouter string `json:"defaultRouter" gorm:"comment:默认菜单;default:dashboard"`
}

权限规则表 (casbin_rule)

go
type CasbinRule struct {
    ID    uint   `gorm:"primaryKey;autoIncrement"`
    Ptype string `gorm:"size:512;uniqueIndex:unique_index"`
    V0    string `gorm:"size:512;uniqueIndex:unique_index"`
    V1    string `gorm:"size:512;uniqueIndex:unique_index"`
    V2    string `gorm:"size:512;uniqueIndex:unique_index"`
    V3    string `gorm:"size:512;uniqueIndex:unique_index"`
    V4    string `gorm:"size:512;uniqueIndex:unique_index"`
    V5    string `gorm:"size:512;uniqueIndex:unique_index"`
}

API 权限表 (sys_apis)

go
type SysApi struct {
    global.GVA_MODEL
    Path        string `json:"path" gorm:"comment:api路径"`
    Description string `json:"description" gorm:"comment:api中文描述"`
    ApiGroup    string `json:"apiGroup" gorm:"comment:api组"`
    Method      string `json:"method" gorm:"default:POST;comment:方法"`
}

🎛️ 权限管理功能

1. 角色管理

创建角色

go
// CreateAuthority 创建角色
func (authorityService *AuthorityService) CreateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) {
    var authorityBox system.SysAuthority
    if !errors.Is(global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&authorityBox).Error, gorm.ErrRecordNotFound) {
        return auth, errors.New("存在相同角色id")
    }
    err = global.GVA_DB.Create(&auth).Error
    return auth, err
}

角色继承

go
// 设置角色继承关系
e := casbinService.Casbin()
e.AddRoleForUser("user1", "role1")  // 用户继承角色
e.AddRoleForUser("role1", "role2")  // 角色继承角色

2. API 权限管理

分配 API 权限

go
// UpdateCasbinApi 更新API权限
func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath string, oldMethod string, newMethod string) error {
    err := global.GVA_DB.Model(&gormadapter.CasbinRule{}).Where("v1 = ? AND v2 = ?", oldPath, oldMethod).Updates(map[string]interface{}{
        "v1": newPath,
        "v2": newMethod,
    }).Error
    e := casbinService.Casbin()
    err = e.LoadPolicy()
    return err
}

API 权限验证

go
// 权限验证示例
func checkPermission(userId, path, method string) bool {
    e := casbinService.Casbin()
    
    // 获取用户角色
    roles := e.GetRolesForUser(userId)
    
    // 检查权限
    for _, role := range roles {
        if ok, _ := e.Enforce(role, path, method); ok {
            return true
        }
    }
    return false
}

3. 菜单权限管理

菜单权限表 (sys_base_menus)

go
type SysBaseMenu struct {
    global.GVA_MODEL
    MenuLevel     uint                                     `json:"-"`
    ParentId      string                                   `json:"parentId" gorm:"comment:父菜单ID"`
    Path          string                                   `json:"path" gorm:"comment:路由path"`
    Name          string                                   `json:"name" gorm:"comment:路由name"`
    Hidden        bool                                     `json:"hidden" gorm:"comment:是否在列表隐藏"`
    Component     string                                   `json:"component" gorm:"comment:对应前端文件路径"`
    Sort          int                                      `json:"sort" gorm:"comment:排序标记"`
    Meta          `json:"meta" gorm:"embedded;comment:附加属性"`
    SysAuthoritys []SysAuthority                          `json:"authoritys" gorm:"many2many:sys_authority_menus;"`
    Children      []SysBaseMenu                           `json:"children" gorm:"-"`
    Parameters    []SysBaseMenuParameter                  `json:"parameters"`
    MenuBtn       []SysBaseMenuBtn                        `json:"menuBtn"`
}

动态菜单生成

go
// GetMenuTree 获取动态菜单树
func (menuService *MenuService) GetMenuTree(authorityId string) (menus []system.SysMenu, err error) {
    menuTree, err := menuService.getMenuTreeMap(authorityId)
    menus = menuTree["0"]
    for i := 0; i < len(menus); i++ {
        err = menuService.getChildrenList(&menus[i], menuTree)
    }
    return menus, err
}

4. 按钮权限管理

按钮权限表 (sys_base_menu_btns)

go
type SysBaseMenuBtn struct {
    global.GVA_MODEL
    Name          string `json:"name" gorm:"comment:按钮关键key"`
    Desc          string `json:"desc" gorm:"comment:按钮备注"`
    SysBaseMenuID uint   `json:"sysBaseMenuID" gorm:"comment:菜单ID"`
}

前端按钮权限控制

vue
<template>
  <!-- 使用 v-auth 指令控制按钮显示 -->
  <el-button v-auth="'user:create'" @click="createUser">
    创建用户
  </el-button>
  
  <el-button v-auth="'user:delete'" @click="deleteUser">
    删除用户
  </el-button>
</template>

<script>
// 权限指令实现
app.directive('auth', {
  mounted(el, binding) {
    const { value } = binding
    const userStore = useUserStore()
    
    if (!userStore.hasPermission(value)) {
      el.style.display = 'none'
    }
  }
})
</script>

🔄 权限同步机制

权限缓存更新

go
// 权限变更时同步缓存
func (casbinService *CasbinService) FreshCasbin() (err error) {
    e := casbinService.Casbin()
    err = e.LoadPolicy()
    return err
}

// 清除用户权限缓存
func (casbinService *CasbinService) ClearUserCache(userId string) {
    // 清除Redis中的用户权限缓存
    global.GVA_REDIS.Del(context.Background(), "user:permissions:"+userId)
}

实时权限验证

go
// 实时权限检查
func (casbinService *CasbinService) CheckPermission(userId, resource, action string) bool {
    // 1. 检查缓存
    cacheKey := fmt.Sprintf("permission:%s:%s:%s", userId, resource, action)
    if result, err := global.GVA_REDIS.Get(context.Background(), cacheKey).Result(); err == nil {
        return result == "true"
    }
    
    // 2. 实时验证
    e := casbinService.Casbin()
    hasPermission, _ := e.Enforce(userId, resource, action)
    
    // 3. 缓存结果
    global.GVA_REDIS.Set(context.Background(), cacheKey, hasPermission, time.Minute*5)
    
    return hasPermission
}

🎨 前端权限集成

路由权限控制

javascript
// router/permission.js
import { useUserStore } from '@/pinia/modules/user'

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  
  // 检查登录状态
  if (!userStore.token) {
    if (to.path !== '/login') {
      return next('/login')
    }
    return next()
  }
  
  // 检查路由权限
  if (to.meta.requiresAuth) {
    const hasPermission = await userStore.checkRoutePermission(to.path)
    if (!hasPermission) {
      return next('/403')
    }
  }
  
  next()
})

API 权限拦截

javascript
// utils/request.js
import axios from 'axios'
import { useUserStore } from '@/pinia/modules/user'

// 请求拦截器
axios.interceptors.request.use(
  config => {
    const userStore = useUserStore()
    
    // 添加 Token
    if (userStore.token) {
      config.headers['x-token'] = userStore.token
    }
    
    return config
  },
  error => Promise.reject(error)
)

// 响应拦截器
axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 403) {
      // 权限不足处理
      ElMessage.error('权限不足')
      return Promise.reject(error)
    }
    
    if (error.response?.status === 401) {
      // Token 过期处理
      const userStore = useUserStore()
      userStore.logout()
      router.push('/login')
    }
    
    return Promise.reject(error)
  }
)

🔒 安全最佳实践

1. 最小权限原则

  • 用户只获得完成工作所需的最小权限
  • 定期审查和清理不必要的权限
  • 实现权限的时效性控制

2. 权限分离

  • 管理权限与业务权限分离
  • 读权限与写权限分离
  • 敏感操作需要额外验证

3. 审计日志

go
// 权限操作日志
type PermissionLog struct {
    UserID    string    `json:"user_id"`
    Action    string    `json:"action"`
    Resource  string    `json:"resource"`
    Result    bool      `json:"result"`
    IP        string    `json:"ip"`
    UserAgent string    `json:"user_agent"`
    Timestamp time.Time `json:"timestamp"`
}

// 记录权限操作
func LogPermissionCheck(userID, action, resource string, result bool, c *gin.Context) {
    log := PermissionLog{
        UserID:    userID,
        Action:    action,
        Resource:  resource,
        Result:    result,
        IP:        c.ClientIP(),
        UserAgent: c.GetHeader("User-Agent"),
        Timestamp: time.Now(),
    }
    
    // 记录到数据库或日志文件
    global.GVA_LOG.Info("Permission Check", zap.Any("log", log))
}

🐛 常见问题

Q: 权限修改后不生效?

A: 需要调用 e.LoadPolicy() 重新加载权限策略,或重启应用。

Q: 如何实现数据权限控制?

A: 可以在 Casbin 规则中添加数据范围字段,或使用自定义的数据过滤器。

Q: 权限验证性能如何优化?

A: 使用 Redis 缓存权限结果,设置合理的缓存过期时间。

Q: 如何实现临时权限?

A: 可以在权限规则中添加时间字段,或使用定时任务清理过期权限。

📚 相关文档