znlgis 博客

GIS开发与技术分享

第四章:权限系统与多租户实现

目录

  1. RBAC权限模型概述
  2. 用户认证机制
  3. 菜单权限控制
  4. 按钮权限控制
  5. 数据权限控制
  6. 多租户架构设计
  7. 租户权限隔离
  8. 权限相关最佳实践

1. RBAC权限模型概述

1.1 什么是RBAC

RBAC(Role-Based Access Control,基于角色的访问控制)是一种广泛使用的权限管理模型。在这种模型中,权限不是直接分配给用户,而是分配给角色,用户通过被分配的角色来获得相应的权限。

Admin.NET采用的是增强型RBAC模型(RBAC1+RBAC2的混合),支持:

1.2 核心概念

用户(User):系统的操作者,可以是企业员工或系统管理员。

角色(Role):权限的集合,一个用户可以拥有多个角色。

权限(Permission):对系统资源的操作许可,包括菜单访问权限、按钮操作权限、数据访问权限。

资源(Resource):系统中的菜单、按钮、API接口、数据等。

1.3 Admin.NET权限模型

┌─────────────────────────────────────────────────────────────────┐
│                           用户 (SysUser)                         │
└─────────────────────────────────────────────────────────────────┘
                               │ 1:N
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│                     用户角色关系 (SysUserRole)                   │
└─────────────────────────────────────────────────────────────────┘
                               │ N:1
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│                           角色 (SysRole)                         │
└─────────────────────────────────────────────────────────────────┘
                               │ 1:N
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│                     角色菜单关系 (SysRoleMenu)                   │
└─────────────────────────────────────────────────────────────────┘
                               │ N:1
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│                           菜单 (SysMenu)                         │
│   ┌─────────┐      ┌─────────┐      ┌─────────┐                 │
│   │  目录   │  ->  │  菜单   │  ->  │  按钮   │                 │
│   └─────────┘      └─────────┘      └─────────┘                 │
└─────────────────────────────────────────────────────────────────┘

1.4 相关数据表

表名 说明 主要字段
SysUser 系统用户表 Id, Account, RealName, OrgId, Status
SysRole 系统角色表 Id, Name, Code, DataScope, Status
SysMenu 系统菜单表 Id, Pid, Type, Title, Permission, Path
SysUserRole 用户角色关系表 UserId, RoleId
SysRoleMenu 角色菜单关系表 RoleId, MenuId
SysRoleOrg 角色机构关系表 RoleId, OrgId

2. 用户认证机制

2.1 JWT认证流程

Admin.NET使用JWT(JSON Web Token)实现用户认证,流程如下:

┌─────────────────────────────────────────────────────────────────┐
│                        登录认证流程                              │
└─────────────────────────────────────────────────────────────────┘

1. 用户提交登录信息
   ┌─────────┐                    ┌─────────┐
   │  前端   │ --- 账号密码 --->  │  后端   │
   └─────────┘                    └─────────┘

2. 后端验证并返回Token
   ┌─────────┐                    ┌─────────┐
   │  前端   │ <--- JWT Token --- │  后端   │
   └─────────┘                    └─────────┘

3. 后续请求携带Token
   ┌─────────┐                    ┌─────────┐
   │  前端   │ --- Token --->     │  后端   │
   │         │ <--- 数据响应 ---  │         │
   └─────────┘                    └─────────┘

2.2 登录认证服务

/// <summary>
/// 系统认证服务
/// </summary>
[ApiDescriptionSettings(Order = 500)]
public class SysAuthService : IDynamicApiController, ITransient
{
    private readonly SqlSugarRepository<SysUser> _sysUserRep;
    private readonly SysCacheService _sysCacheService;
    private readonly SysConfigService _sysConfigService;

    public SysAuthService(
        SqlSugarRepository<SysUser> sysUserRep,
        SysCacheService sysCacheService,
        SysConfigService sysConfigService)
    {
        _sysUserRep = sysUserRep;
        _sysCacheService = sysCacheService;
        _sysConfigService = sysConfigService;
    }

    /// <summary>
    /// 用户登录
    /// </summary>
    [AllowAnonymous]
    [DisplayName("用户登录")]
    public async Task<LoginOutput> Login(LoginInput input)
    {
        // 1. 验证验证码
        if (!await ValidateCaptcha(input.CaptchaId, input.CaptchaCode))
            throw Oops.Oh(ErrorCodeEnum.D0008);

        // 2. 获取用户信息
        var user = await _sysUserRep.AsQueryable()
            .Filter(null, true)  // 忽略所有过滤器
            .FirstAsync(u => u.Account == input.Account);

        if (user == null)
            throw Oops.Oh(ErrorCodeEnum.D0009);

        // 3. 验证用户状态
        if (user.Status == StatusEnum.Disable)
            throw Oops.Oh(ErrorCodeEnum.D1017);

        // 4. 验证密码
        var encryptPassword = CryptogramUtil.Encrypt(input.Password);
        if (user.Password != encryptPassword)
        {
            // 记录登录失败次数
            await RecordLoginFail(user);
            throw Oops.Oh(ErrorCodeEnum.D1000);
        }

        // 5. 生成Token
        var accessToken = GenerateToken(user);
        var refreshToken = GenerateRefreshToken(user);

        // 6. 记录登录日志
        await RecordLoginLog(user, true);

        // 7. 缓存用户信息
        await CacheUserInfo(user);

        return new LoginOutput
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken,
            Expire = GetTokenExpire()
        };
    }

    /// <summary>
    /// 生成JWT Token
    /// </summary>
    private string GenerateToken(SysUser user)
    {
        var claims = new[]
        {
            new Claim(ClaimConst.UserId, user.Id.ToString()),
            new Claim(ClaimConst.Account, user.Account),
            new Claim(ClaimConst.RealName, user.RealName ?? ""),
            new Claim(ClaimConst.AccountType, ((int)user.AccountType).ToString()),
            new Claim(ClaimConst.OrgId, user.OrgId.ToString()),
            new Claim(ClaimConst.TenantId, user.TenantId?.ToString() ?? ""),
        };

        return JWTEncryption.Encrypt(claims);
    }

    /// <summary>
    /// 获取用户信息
    /// </summary>
    [DisplayName("获取用户信息")]
    public async Task<LoginUserOutput> GetUserInfo()
    {
        var userId = App.User.FindFirstValue(ClaimConst.UserId);
        var user = await _sysUserRep.GetByIdAsync(long.Parse(userId));

        // 获取用户角色
        var roles = await GetUserRoles(user.Id);

        // 获取用户权限
        var permissions = await GetUserPermissions(user.Id);

        // 获取用户菜单
        var menus = await GetUserMenus(user.Id);

        return new LoginUserOutput
        {
            Id = user.Id,
            Account = user.Account,
            RealName = user.RealName,
            Avatar = user.Avatar,
            OrgId = user.OrgId,
            Roles = roles,
            Permissions = permissions,
            Menus = menus
        };
    }

    /// <summary>
    /// 刷新Token
    /// </summary>
    [AllowAnonymous]
    [DisplayName("刷新Token")]
    public async Task<LoginOutput> RefreshToken(string refreshToken)
    {
        // 验证RefreshToken
        var principal = JWTEncryption.ReadJwtToken(refreshToken);
        if (principal == null)
            throw Oops.Oh(ErrorCodeEnum.D1012);

        var userId = principal.Claims
            .FirstOrDefault(c => c.Type == ClaimConst.UserId)?.Value;

        var user = await _sysUserRep.GetByIdAsync(long.Parse(userId));
        if (user == null || user.Status == StatusEnum.Disable)
            throw Oops.Oh(ErrorCodeEnum.D1017);

        // 生成新Token
        var accessToken = GenerateToken(user);
        var newRefreshToken = GenerateRefreshToken(user);

        return new LoginOutput
        {
            AccessToken = accessToken,
            RefreshToken = newRefreshToken,
            Expire = GetTokenExpire()
        };
    }

    /// <summary>
    /// 退出登录
    /// </summary>
    [DisplayName("退出登录")]
    public async Task Logout()
    {
        var userId = App.User.FindFirstValue(ClaimConst.UserId);
        
        // 清除用户缓存
        _sysCacheService.Remove(CacheConst.KeyUserInfo + userId);
        _sysCacheService.Remove(CacheConst.KeyUserMenu + userId);
        _sysCacheService.Remove(CacheConst.KeyUserPermission + userId);

        // 记录退出日志
        await RecordLogoutLog(long.Parse(userId));

        // 通知前端退出
        await App.GetService<IHubContext<OnlineUserHub>>()
            .Clients.User(userId)
            .SendAsync("Logout");
    }
}

2.3 JWT处理器

/// <summary>
/// JWT授权处理器
/// </summary>
public class JwtHandler : AppAuthorizeHandler
{
    /// <summary>
    /// 授权判断
    /// </summary>
    public override async Task HandleAsync(AuthorizationHandlerContext context)
    {
        // 判断是否授权
        var isAuthenticated = context.User.Identity?.IsAuthenticated ?? false;
        if (!isAuthenticated)
        {
            context.Fail();
            return;
        }

        // 自动刷新Token
        if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext()))
        {
            await AuthorizeHandleAsync(context);
        }
        else
        {
            context.Fail();
        }
    }

    /// <summary>
    /// 授权处理
    /// </summary>
    public override async Task<bool> PipelineAsync(
        AuthorizationHandlerContext context, 
        DefaultHttpContext httpContext)
    {
        // 获取用户Id
        var userId = context.User.FindFirstValue(ClaimConst.UserId);
        if (string.IsNullOrEmpty(userId))
            return false;

        // 获取用户信息
        var cache = App.GetService<SysCacheService>();
        var user = cache.Get<SysUser>(CacheConst.KeyUserInfo + userId);

        if (user == null)
        {
            // 缓存不存在,从数据库获取
            var userRep = App.GetService<SqlSugarRepository<SysUser>>();
            user = await userRep.GetByIdAsync(long.Parse(userId));

            if (user == null || user.Status == StatusEnum.Disable)
                return false;

            // 写入缓存
            cache.Set(CacheConst.KeyUserInfo + userId, user, TimeSpan.FromHours(2));
        }

        // 超级管理员放行
        if (user.AccountType == AccountTypeEnum.SuperAdmin)
            return true;

        // 路由权限判断
        return await CheckPermission(context, httpContext, user);
    }

    /// <summary>
    /// 检查权限
    /// </summary>
    private async Task<bool> CheckPermission(
        AuthorizationHandlerContext context,
        DefaultHttpContext httpContext,
        SysUser user)
    {
        // 获取当前请求的路由信息
        var endpoint = httpContext.GetEndpoint();
        
        // 获取权限标识特性
        var permissionAttr = endpoint?.Metadata.GetMetadata<PermissionAttribute>();
        if (permissionAttr == null)
            return true; // 没有权限标识的接口放行

        // 获取用户权限列表
        var cache = App.GetService<SysCacheService>();
        var permissions = cache.Get<List<string>>(CacheConst.KeyUserPermission + user.Id);

        if (permissions == null)
        {
            // 从数据库获取
            var menuService = App.GetService<SysMenuService>();
            permissions = await menuService.GetUserPermissionList(user.Id);
            cache.Set(CacheConst.KeyUserPermission + user.Id, permissions, TimeSpan.FromHours(2));
        }

        // 判断是否有权限
        return permissions.Contains(permissionAttr.Permission);
    }
}

2.4 登录日志记录

/// <summary>
/// 记录登录日志
/// </summary>
private async Task RecordLoginLog(SysUser user, bool success)
{
    var httpContext = App.HttpContext;
    var ip = httpContext.GetRemoteIpAddressToIPv4();
    var userAgent = httpContext.Request.Headers["User-Agent"];

    var log = new SysLogVis
    {
        Account = user.Account,
        RealName = user.RealName,
        Success = success ? YesNoEnum.Y : YesNoEnum.N,
        Message = success ? "登录成功" : "登录失败",
        Ip = ip,
        Location = GetLocation(ip),
        Browser = GetBrowser(userAgent),
        Os = GetOs(userAgent),
        VisType = LoginTypeEnum.Login,
        VisTime = DateTime.Now
    };

    await _sysLogVisRep.InsertAsync(log);
}

3. 菜单权限控制

3.1 菜单实体结构

/// <summary>
/// 系统菜单表
/// </summary>
[SugarTable(null, "系统菜单表")]
public class SysMenu : EntityBase
{
    /// <summary>
    /// 父Id
    /// </summary>
    [SugarColumn(ColumnDescription = "父Id")]
    public long Pid { get; set; }

    /// <summary>
    /// 菜单类型(1目录 2菜单 3按钮)
    /// </summary>
    [SugarColumn(ColumnDescription = "菜单类型")]
    public MenuTypeEnum Type { get; set; }

    /// <summary>
    /// 菜单名称
    /// </summary>
    [SugarColumn(ColumnDescription = "菜单名称", Length = 64)]
    public string Title { get; set; }

    /// <summary>
    /// 路由名称
    /// </summary>
    [SugarColumn(ColumnDescription = "路由名称", Length = 64)]
    public string? Name { get; set; }

    /// <summary>
    /// 路由地址
    /// </summary>
    [SugarColumn(ColumnDescription = "路由地址", Length = 128)]
    public string? Path { get; set; }

    /// <summary>
    /// 组件路径
    /// </summary>
    [SugarColumn(ColumnDescription = "组件路径", Length = 128)]
    public string? Component { get; set; }

    /// <summary>
    /// 权限标识
    /// </summary>
    [SugarColumn(ColumnDescription = "权限标识", Length = 128)]
    public string? Permission { get; set; }

    /// <summary>
    /// 菜单图标
    /// </summary>
    [SugarColumn(ColumnDescription = "菜单图标", Length = 64)]
    public string? Icon { get; set; }

    /// <summary>
    /// 是否隐藏
    /// </summary>
    [SugarColumn(ColumnDescription = "是否隐藏")]
    public bool IsHide { get; set; }

    /// <summary>
    /// 是否缓存
    /// </summary>
    [SugarColumn(ColumnDescription = "是否缓存")]
    public bool IsKeepAlive { get; set; } = true;

    /// <summary>
    /// 排序
    /// </summary>
    [SugarColumn(ColumnDescription = "排序")]
    public int OrderNo { get; set; } = 100;

    /// <summary>
    /// 状态
    /// </summary>
    [SugarColumn(ColumnDescription = "状态")]
    public StatusEnum Status { get; set; } = StatusEnum.Enable;
}

3.2 菜单服务

/// <summary>
/// 系统菜单服务
/// </summary>
[ApiDescriptionSettings(Order = 480)]
public class SysMenuService : IDynamicApiController, ITransient
{
    private readonly SqlSugarRepository<SysMenu> _sysMenuRep;
    private readonly SqlSugarRepository<SysRoleMenu> _sysRoleMenuRep;
    private readonly SqlSugarRepository<SysUserRole> _sysUserRoleRep;
    private readonly SysCacheService _sysCacheService;
    private readonly IUserManager _userManager;

    public SysMenuService(
        SqlSugarRepository<SysMenu> sysMenuRep,
        SqlSugarRepository<SysRoleMenu> sysRoleMenuRep,
        SqlSugarRepository<SysUserRole> sysUserRoleRep,
        SysCacheService sysCacheService,
        IUserManager userManager)
    {
        _sysMenuRep = sysMenuRep;
        _sysRoleMenuRep = sysRoleMenuRep;
        _sysUserRoleRep = sysUserRoleRep;
        _sysCacheService = sysCacheService;
        _userManager = userManager;
    }

    /// <summary>
    /// 获取用户菜单列表
    /// </summary>
    [DisplayName("获取用户菜单列表")]
    public async Task<List<SysMenu>> GetLoginMenuList()
    {
        // 超级管理员获取所有菜单
        if (_userManager.SuperAdmin)
        {
            return await _sysMenuRep.AsQueryable()
                .Where(m => m.Type != MenuTypeEnum.Btn && m.Status == StatusEnum.Enable)
                .OrderBy(m => m.OrderNo)
                .ToTreeAsync(m => m.Children, m => m.Pid, 0);
        }

        // 普通用户获取授权菜单
        var userId = _userManager.UserId;

        // 先查缓存
        var cacheKey = CacheConst.KeyUserMenu + userId;
        var menus = _sysCacheService.Get<List<SysMenu>>(cacheKey);

        if (menus != null)
            return menus;

        // 获取用户角色
        var roleIds = await _sysUserRoleRep.AsQueryable()
            .Where(ur => ur.UserId == userId)
            .Select(ur => ur.RoleId)
            .ToListAsync();

        if (!roleIds.Any())
            return new List<SysMenu>();

        // 获取角色菜单
        var menuIds = await _sysRoleMenuRep.AsQueryable()
            .Where(rm => roleIds.Contains(rm.RoleId))
            .Select(rm => rm.MenuId)
            .Distinct()
            .ToListAsync();

        // 获取菜单详情
        menus = await _sysMenuRep.AsQueryable()
            .Where(m => menuIds.Contains(m.Id))
            .Where(m => m.Type != MenuTypeEnum.Btn && m.Status == StatusEnum.Enable)
            .OrderBy(m => m.OrderNo)
            .ToTreeAsync(m => m.Children, m => m.Pid, 0);

        // 写入缓存
        _sysCacheService.Set(cacheKey, menus, TimeSpan.FromHours(2));

        return menus;
    }

    /// <summary>
    /// 获取用户权限标识列表
    /// </summary>
    public async Task<List<string>> GetUserPermissionList(long userId)
    {
        // 超级管理员返回所有权限
        if (_userManager.SuperAdmin)
        {
            return await _sysMenuRep.AsQueryable()
                .Where(m => !string.IsNullOrEmpty(m.Permission))
                .Select(m => m.Permission)
                .Distinct()
                .ToListAsync();
        }

        // 获取用户角色
        var roleIds = await _sysUserRoleRep.AsQueryable()
            .Where(ur => ur.UserId == userId)
            .Select(ur => ur.RoleId)
            .ToListAsync();

        if (!roleIds.Any())
            return new List<string>();

        // 获取角色菜单权限
        var menuIds = await _sysRoleMenuRep.AsQueryable()
            .Where(rm => roleIds.Contains(rm.RoleId))
            .Select(rm => rm.MenuId)
            .Distinct()
            .ToListAsync();

        // 获取权限标识
        return await _sysMenuRep.AsQueryable()
            .Where(m => menuIds.Contains(m.Id))
            .Where(m => !string.IsNullOrEmpty(m.Permission))
            .Select(m => m.Permission)
            .Distinct()
            .ToListAsync();
    }

    /// <summary>
    /// 添加菜单
    /// </summary>
    [ApiDescriptionSettings(Name = "Add"), HttpPost]
    [DisplayName("添加菜单")]
    public async Task<long> Add(AddMenuInput input)
    {
        // 验证菜单名称是否重复
        var exist = await _sysMenuRep.IsAnyAsync(m => m.Title == input.Title && m.Pid == input.Pid);
        if (exist)
            throw Oops.Oh(ErrorCodeEnum.D4000);

        var menu = input.Adapt<SysMenu>();
        await _sysMenuRep.InsertAsync(menu);

        // 清除所有用户的菜单缓存
        _sysCacheService.RemoveByPrefix(CacheConst.KeyUserMenu);
        _sysCacheService.RemoveByPrefix(CacheConst.KeyUserPermission);

        return menu.Id;
    }

    /// <summary>
    /// 删除菜单
    /// </summary>
    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
    [DisplayName("删除菜单")]
    public async Task Delete(DeleteMenuInput input)
    {
        // 检查是否有子菜单
        var hasChildren = await _sysMenuRep.IsAnyAsync(m => m.Pid == input.Id);
        if (hasChildren)
            throw Oops.Oh(ErrorCodeEnum.D4001);

        // 删除菜单
        await _sysMenuRep.DeleteByIdAsync(input.Id);

        // 删除角色菜单关系
        await _sysRoleMenuRep.DeleteAsync(rm => rm.MenuId == input.Id);

        // 清除缓存
        _sysCacheService.RemoveByPrefix(CacheConst.KeyUserMenu);
        _sysCacheService.RemoveByPrefix(CacheConst.KeyUserPermission);
    }
}

3.3 前端菜单渲染

// stores/modules/menu.ts
import { defineStore } from 'pinia';
import { menuApi } from '/@/api/system/menu';

export const useMenuStore = defineStore('menu', {
    state: () => ({
        menuList: [] as Menu[],
        menuLoaded: false
    }),
    
    actions: {
        // 设置菜单
        setMenuList(list: Menu[]) {
            this.menuList = list;
            this.menuLoaded = true;
        },
        
        // 获取菜单
        async getMenuList() {
            if (this.menuLoaded) {
                return this.menuList;
            }
            
            const res = await menuApi().getLoginMenuList();
            this.setMenuList(res.data);
            return res.data;
        },
        
        // 转换为路由格式
        formatMenuToRoute(menus: Menu[]): RouteRecordRaw[] {
            const routes: RouteRecordRaw[] = [];
            
            menus.forEach(menu => {
                const route: RouteRecordRaw = {
                    path: menu.path,
                    name: menu.name,
                    component: loadComponent(menu.component),
                    meta: {
                        title: menu.title,
                        icon: menu.icon,
                        isHide: menu.isHide,
                        isKeepAlive: menu.isKeepAlive
                    }
                };
                
                if (menu.children && menu.children.length > 0) {
                    route.children = this.formatMenuToRoute(menu.children);
                }
                
                routes.push(route);
            });
            
            return routes;
        }
    }
});

4. 按钮权限控制

4.1 权限标识定义

按钮权限通过权限标识(Permission)来控制,格式通常为:模块:操作

示例:
- sysUser:add      用户新增
- sysUser:edit     用户编辑
- sysUser:delete   用户删除
- sysUser:export   用户导出

4.2 后端权限特性

/// <summary>
/// 权限特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class PermissionAttribute : Attribute
{
    /// <summary>
    /// 权限标识
    /// </summary>
    public string Permission { get; set; }

    /// <summary>
    /// 权限描述
    /// </summary>
    public string Description { get; set; }

    public PermissionAttribute(string permission)
    {
        Permission = permission;
    }

    public PermissionAttribute(string permission, string description)
    {
        Permission = permission;
        Description = description;
    }
}

// 使用示例
[Permission("sysUser:add", "新增用户")]
[DisplayName("新增用户")]
public async Task<long> Add(AddUserInput input)
{
    // 业务逻辑
}

4.3 前端按钮权限指令

// directives/auth.ts
import { useUserStore } from '/@/stores/modules/user';

export const authDirective = {
    mounted(el: HTMLElement, binding: DirectiveBinding) {
        const userStore = useUserStore();
        const permission = binding.value;
        
        // 检查是否有权限
        if (!userStore.permissions.includes(permission)) {
            // 没有权限则移除元素
            el.parentNode?.removeChild(el);
        }
    }
};

// 注册指令
app.directive('auth', authDirective);
<!-- 使用示例 -->
<template>
    <div class="button-group">
        <el-button v-auth="'sysUser:add'" type="primary" @click="handleAdd">
            新增
        </el-button>
        
        <el-button v-auth="'sysUser:edit'" type="warning" @click="handleEdit">
            编辑
        </el-button>
        
        <el-button v-auth="'sysUser:delete'" type="danger" @click="handleDelete">
            删除
        </el-button>
    </div>
</template>

4.4 权限Hook封装

// hooks/useAuth.ts
import { useUserStore } from '/@/stores/modules/user';

export function useAuth() {
    const userStore = useUserStore();
    
    /**
     * 检查是否有某个权限
     */
    const hasPermission = (permission: string): boolean => {
        return userStore.permissions.includes(permission);
    };
    
    /**
     * 检查是否有任一权限
     */
    const hasAnyPermission = (permissions: string[]): boolean => {
        return permissions.some(p => userStore.permissions.includes(p));
    };
    
    /**
     * 检查是否有所有权限
     */
    const hasAllPermissions = (permissions: string[]): boolean => {
        return permissions.every(p => userStore.permissions.includes(p));
    };
    
    /**
     * 检查是否有某个角色
     */
    const hasRole = (role: string): boolean => {
        return userStore.roles.includes(role);
    };
    
    return {
        hasPermission,
        hasAnyPermission,
        hasAllPermissions,
        hasRole
    };
}

// 使用示例
const { hasPermission, hasRole } = useAuth();

if (hasPermission('sysUser:add')) {
    // 有新增权限
}

if (hasRole('admin')) {
    // 是管理员角色
}

5. 数据权限控制

5.1 数据权限范围

Admin.NET支持以下数据权限范围:

枚举值 说明 描述
All 全部数据 可以访问所有数据
OrgWithChild 本部门及以下 可以访问本部门及所有子部门的数据
Org 本部门 只能访问本部门的数据
Self 仅本人 只能访问自己创建的数据
Custom 自定义 自定义选择可访问的部门
/// <summary>
/// 数据权限范围枚举
/// </summary>
public enum DataScopeEnum
{
    /// <summary>
    /// 全部数据
    /// </summary>
    [Description("全部数据")]
    All = 1,

    /// <summary>
    /// 本部门及以下数据
    /// </summary>
    [Description("本部门及以下数据")]
    OrgWithChild = 2,

    /// <summary>
    /// 本部门数据
    /// </summary>
    [Description("本部门数据")]
    Org = 3,

    /// <summary>
    /// 仅本人数据
    /// </summary>
    [Description("仅本人数据")]
    Self = 4,

    /// <summary>
    /// 自定义数据
    /// </summary>
    [Description("自定义数据")]
    Custom = 5
}

5.2 数据权限过滤器

/// <summary>
/// 数据权限过滤器特性
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class DataScopeFilterAttribute : ActionFilterAttribute
{
    /// <summary>
    /// 是否忽略过滤
    /// </summary>
    public bool IgnoreFilter { get; set; }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, 
        ActionExecutionDelegate next)
    {
        if (IgnoreFilter)
        {
            await next();
            return;
        }

        var userManager = App.GetService<IUserManager>();
        
        // 超级管理员不限制
        if (userManager.SuperAdmin)
        {
            await next();
            return;
        }

        // 获取数据权限范围
        var dataScope = await GetUserDataScope(userManager.UserId);
        
        // 设置过滤条件
        SetDataScopeFilter(dataScope, userManager);

        await next();
    }

    private async Task<DataScopeInfo> GetUserDataScope(long userId)
    {
        var roleService = App.GetService<SysRoleService>();
        return await roleService.GetUserDataScope(userId);
    }

    private void SetDataScopeFilter(DataScopeInfo dataScope, IUserManager userManager)
    {
        var db = App.GetService<ISqlSugarClient>();
        
        switch (dataScope.Scope)
        {
            case DataScopeEnum.All:
                // 不添加过滤条件
                break;

            case DataScopeEnum.OrgWithChild:
                // 本部门及子部门
                db.QueryFilter.AddTableFilter<IDataScope>(d => 
                    dataScope.OrgIds.Contains(d.CreateOrgId.Value));
                break;

            case DataScopeEnum.Org:
                // 本部门
                db.QueryFilter.AddTableFilter<IDataScope>(d => 
                    d.CreateOrgId == userManager.OrgId);
                break;

            case DataScopeEnum.Self:
                // 仅本人
                db.QueryFilter.AddTableFilter<IDataScope>(d => 
                    d.CreateUserId == userManager.UserId);
                break;

            case DataScopeEnum.Custom:
                // 自定义部门
                db.QueryFilter.AddTableFilter<IDataScope>(d => 
                    dataScope.OrgIds.Contains(d.CreateOrgId.Value));
                break;
        }
    }
}

5.3 数据权限接口

/// <summary>
/// 数据权限接口
/// </summary>
public interface IDataScope
{
    /// <summary>
    /// 创建者部门Id
    /// </summary>
    long? CreateOrgId { get; set; }

    /// <summary>
    /// 创建者Id
    /// </summary>
    long? CreateUserId { get; set; }
}

/// <summary>
/// 实体实现数据权限接口
/// </summary>
public abstract class EntityTenant : EntityBaseData, IDataScope
{
    /// <summary>
    /// 租户Id
    /// </summary>
    [SugarColumn(ColumnDescription = "租户Id")]
    public virtual long? TenantId { get; set; }

    /// <summary>
    /// 创建者部门Id
    /// </summary>
    [SugarColumn(ColumnDescription = "创建者部门Id")]
    public virtual long? CreateOrgId { get; set; }
}

5.4 使用数据权限

/// <summary>
/// 业务服务 - 应用数据权限
/// </summary>
public class BusinessService : IDynamicApiController, ITransient
{
    private readonly SqlSugarRepository<Business> _businessRep;

    public BusinessService(SqlSugarRepository<Business> businessRep)
    {
        _businessRep = businessRep;
    }

    /// <summary>
    /// 获取业务列表 - 应用数据权限过滤
    /// </summary>
    [DataScopeFilter]
    [DisplayName("获取业务列表")]
    public async Task<List<Business>> GetList()
    {
        // 查询会自动应用数据权限过滤
        return await _businessRep.AsQueryable()
            .Where(b => b.Status == StatusEnum.Enable)
            .ToListAsync();
    }

    /// <summary>
    /// 获取业务详情 - 忽略数据权限
    /// </summary>
    [DataScopeFilter(IgnoreFilter = true)]
    [DisplayName("获取业务详情")]
    public async Task<Business> GetDetail(long id)
    {
        // 不应用数据权限过滤
        return await _businessRep.GetByIdAsync(id);
    }
}

6. 多租户架构设计

6.1 多租户模式

Admin.NET支持多种多租户模式:

共享数据库模式(默认)

独立数据库模式

混合模式

6.2 租户实体

/// <summary>
/// 系统租户表
/// </summary>
[SugarTable(null, "系统租户表")]
[SystemTable]
public class SysTenant : EntityBase
{
    /// <summary>
    /// 租户名称
    /// </summary>
    [SugarColumn(ColumnDescription = "租户名称", Length = 64)]
    public string Name { get; set; }

    /// <summary>
    /// 租户编码
    /// </summary>
    [SugarColumn(ColumnDescription = "租户编码", Length = 64)]
    public string Code { get; set; }

    /// <summary>
    /// 管理员账号
    /// </summary>
    [SugarColumn(ColumnDescription = "管理员账号")]
    public long AdminId { get; set; }

    /// <summary>
    /// 数据库类型
    /// </summary>
    [SugarColumn(ColumnDescription = "数据库类型")]
    public DbType? DbType { get; set; }

    /// <summary>
    /// 数据库连接字符串
    /// </summary>
    [SugarColumn(ColumnDescription = "数据库连接字符串", Length = 512)]
    public string? ConnectionString { get; set; }

    /// <summary>
    /// 状态
    /// </summary>
    [SugarColumn(ColumnDescription = "状态")]
    public StatusEnum Status { get; set; } = StatusEnum.Enable;

    /// <summary>
    /// 备注
    /// </summary>
    [SugarColumn(ColumnDescription = "备注", Length = 256)]
    public string? Remark { get; set; }

    /// <summary>
    /// 租户套餐
    /// </summary>
    [SugarColumn(ColumnDescription = "租户套餐")]
    public long? PackageId { get; set; }

    /// <summary>
    /// 到期时间
    /// </summary>
    [SugarColumn(ColumnDescription = "到期时间")]
    public DateTime? ExpireTime { get; set; }
}

6.3 租户过滤器

/// <summary>
/// 租户过滤器
/// </summary>
public class TenantEntityFilter : IEntityFilter
{
    public Expression<Func<T, bool>> GetFilter<T>() where T : class
    {
        // 检查是否是租户实体
        if (!typeof(T).IsAssignableTo(typeof(EntityTenant)))
            return null;

        // 获取当前租户Id
        var userManager = App.GetService<IUserManager>();
        var tenantId = userManager?.TenantId;

        // 超级管理员不限制
        if (userManager?.SuperAdmin == true)
            return null;

        // 构建过滤表达式
        return u => (u as EntityTenant).TenantId == tenantId;
    }
}

6.4 动态数据库切换

/// <summary>
/// 租户数据库管理
/// </summary>
public class TenantDbManager : ISingleton
{
    private readonly ISqlSugarClient _db;
    private readonly SysCacheService _cache;

    public TenantDbManager(ISqlSugarClient db, SysCacheService cache)
    {
        _db = db;
        _cache = cache;
    }

    /// <summary>
    /// 获取租户数据库连接
    /// </summary>
    public ISqlSugarClient GetTenantDb(long tenantId)
    {
        // 获取租户配置
        var tenant = _cache.Get<SysTenant>(CacheConst.KeyTenant + tenantId);
        
        if (tenant == null)
        {
            tenant = _db.Queryable<SysTenant>()
                .First(t => t.Id == tenantId);
            
            if (tenant != null)
                _cache.Set(CacheConst.KeyTenant + tenantId, tenant);
        }

        // 如果租户有独立数据库
        if (!string.IsNullOrEmpty(tenant?.ConnectionString))
        {
            return new SqlSugarClient(new ConnectionConfig
            {
                ConfigId = tenantId,
                DbType = tenant.DbType ?? SqlSugar.DbType.MySql,
                ConnectionString = tenant.ConnectionString,
                IsAutoCloseConnection = true
            });
        }

        // 使用默认数据库
        return _db;
    }

    /// <summary>
    /// 初始化租户数据库
    /// </summary>
    public async Task InitTenantDb(SysTenant tenant)
    {
        if (string.IsNullOrEmpty(tenant.ConnectionString))
            return;

        var tenantDb = GetTenantDb(tenant.Id);

        // 创建表结构
        tenantDb.CodeFirst.InitTables(
            typeof(Business),
            typeof(Order),
            // ... 其他业务表
        );

        // 初始化种子数据
        await InitTenantSeedData(tenantDb, tenant);
    }
}

7. 租户权限隔离

7.1 租户菜单管理

/// <summary>
/// 租户菜单关系表
/// </summary>
[SugarTable(null, "租户菜单关系表")]
public class SysTenantMenu : EntityBase
{
    /// <summary>
    /// 租户Id
    /// </summary>
    [SugarColumn(ColumnDescription = "租户Id")]
    public long TenantId { get; set; }

    /// <summary>
    /// 菜单Id
    /// </summary>
    [SugarColumn(ColumnDescription = "菜单Id")]
    public long MenuId { get; set; }
}

/// <summary>
/// 租户菜单服务
/// </summary>
public class SysTenantMenuService : IDynamicApiController, ITransient
{
    private readonly SqlSugarRepository<SysTenantMenu> _tenantMenuRep;
    private readonly SqlSugarRepository<SysMenu> _menuRep;

    /// <summary>
    /// 获取租户授权的菜单
    /// </summary>
    public async Task<List<SysMenu>> GetTenantMenuList(long tenantId)
    {
        var menuIds = await _tenantMenuRep.AsQueryable()
            .Where(tm => tm.TenantId == tenantId)
            .Select(tm => tm.MenuId)
            .ToListAsync();

        return await _menuRep.AsQueryable()
            .Where(m => menuIds.Contains(m.Id))
            .Where(m => m.Status == StatusEnum.Enable)
            .OrderBy(m => m.OrderNo)
            .ToListAsync();
    }

    /// <summary>
    /// 授权租户菜单
    /// </summary>
    public async Task GrantTenantMenu(GrantTenantMenuInput input)
    {
        // 删除旧的授权
        await _tenantMenuRep.DeleteAsync(tm => tm.TenantId == input.TenantId);

        // 添加新的授权
        var tenantMenus = input.MenuIds.Select(menuId => new SysTenantMenu
        {
            TenantId = input.TenantId,
            MenuId = menuId
        }).ToList();

        await _tenantMenuRep.InsertRangeAsync(tenantMenus);
    }
}

7.2 租户数据隔离

/// <summary>
/// 租户数据服务基类
/// </summary>
public abstract class TenantBaseService<TEntity> where TEntity : EntityTenant, new()
{
    protected readonly SqlSugarRepository<TEntity> _rep;
    protected readonly IUserManager _userManager;

    protected TenantBaseService(SqlSugarRepository<TEntity> rep, IUserManager userManager)
    {
        _rep = rep;
        _userManager = userManager;
    }

    /// <summary>
    /// 获取当前租户的查询
    /// </summary>
    protected ISugarQueryable<TEntity> GetTenantQuery()
    {
        var query = _rep.AsQueryable();
        
        // 超级管理员不限制租户
        if (!_userManager.SuperAdmin)
        {
            query = query.Where(e => e.TenantId == _userManager.TenantId);
        }
        
        return query;
    }

    /// <summary>
    /// 添加实体(自动设置租户Id)
    /// </summary>
    protected async Task<long> InsertWithTenant(TEntity entity)
    {
        entity.TenantId = _userManager.TenantId;
        await _rep.InsertAsync(entity);
        return entity.Id;
    }

    /// <summary>
    /// 更新实体(验证租户)
    /// </summary>
    protected async Task UpdateWithTenant(TEntity entity)
    {
        // 验证数据归属
        var exists = await _rep.IsAnyAsync(e => 
            e.Id == entity.Id && e.TenantId == _userManager.TenantId);
        
        if (!exists)
            throw Oops.Oh(ErrorCodeEnum.D1002);
        
        await _rep.UpdateAsync(entity);
    }

    /// <summary>
    /// 删除实体(验证租户)
    /// </summary>
    protected async Task DeleteWithTenant(long id)
    {
        var exists = await _rep.IsAnyAsync(e => 
            e.Id == id && e.TenantId == _userManager.TenantId);
        
        if (!exists)
            throw Oops.Oh(ErrorCodeEnum.D1002);
        
        await _rep.DeleteByIdAsync(id);
    }
}

7.3 租户配置管理

/// <summary>
/// 租户配置服务
/// </summary>
public class SysTenantConfigService : IDynamicApiController, ITransient
{
    private readonly SqlSugarRepository<SysTenantConfig> _configRep;
    private readonly SysCacheService _cache;

    /// <summary>
    /// 获取租户配置
    /// </summary>
    public async Task<T> GetConfig<T>(long tenantId, string key) where T : class
    {
        var cacheKey = $"{CacheConst.KeyTenantConfig}:{tenantId}:{key}";
        var value = _cache.Get<T>(cacheKey);

        if (value != null)
            return value;

        var config = await _configRep.GetFirstAsync(c => 
            c.TenantId == tenantId && c.Key == key);

        if (config != null)
        {
            value = JSON.Deserialize<T>(config.Value);
            _cache.Set(cacheKey, value, TimeSpan.FromHours(24));
        }

        return value;
    }

    /// <summary>
    /// 设置租户配置
    /// </summary>
    public async Task SetConfig<T>(long tenantId, string key, T value)
    {
        var config = await _configRep.GetFirstAsync(c => 
            c.TenantId == tenantId && c.Key == key);

        if (config == null)
        {
            config = new SysTenantConfig
            {
                TenantId = tenantId,
                Key = key,
                Value = JSON.Serialize(value)
            };
            await _configRep.InsertAsync(config);
        }
        else
        {
            config.Value = JSON.Serialize(value);
            await _configRep.UpdateAsync(config);
        }

        // 清除缓存
        var cacheKey = $"{CacheConst.KeyTenantConfig}:{tenantId}:{key}";
        _cache.Remove(cacheKey);
    }
}

8. 权限相关最佳实践

8.1 权限设计原则

最小权限原则: 用户只应该拥有完成工作所需的最小权限集合。

// 示例:创建角色时,默认不分配任何权限
public async Task<long> CreateRole(AddRoleInput input)
{
    var role = new SysRole
    {
        Name = input.Name,
        Code = input.Code,
        DataScope = DataScopeEnum.Self, // 默认最小数据权限
        Status = StatusEnum.Enable
    };
    
    await _roleRep.InsertAsync(role);
    
    // 不自动分配菜单权限,需要管理员手动授权
    
    return role.Id;
}

职责分离原则: 关键操作需要多个角色协同完成。

// 示例:敏感操作需要双重验证
[Permission("finance:audit")]
[DisplayName("财务审核")]
public async Task FinanceAudit(long id)
{
    var order = await _orderRep.GetByIdAsync(id);
    
    // 验证是否为不同人员操作
    if (order.CreateUserId == _userManager.UserId)
        throw Oops.Oh("创建人不能审核自己的单据");
    
    // 执行审核逻辑
}

8.2 缓存策略

合理使用缓存: 权限数据变化频率低,适合缓存。

public class PermissionCacheStrategy
{
    private readonly SysCacheService _cache;
    private const int CACHE_HOURS = 2;

    /// <summary>
    /// 获取用户权限(带缓存)
    /// </summary>
    public async Task<List<string>> GetUserPermissions(long userId)
    {
        var cacheKey = CacheConst.KeyUserPermission + userId;
        var permissions = _cache.Get<List<string>>(cacheKey);

        if (permissions == null)
        {
            permissions = await LoadPermissionsFromDb(userId);
            _cache.Set(cacheKey, permissions, TimeSpan.FromHours(CACHE_HOURS));
        }

        return permissions;
    }

    /// <summary>
    /// 清除用户权限缓存
    /// </summary>
    public void ClearUserPermissionCache(long userId)
    {
        _cache.Remove(CacheConst.KeyUserPermission + userId);
        _cache.Remove(CacheConst.KeyUserMenu + userId);
    }

    /// <summary>
    /// 清除角色相关用户的权限缓存
    /// </summary>
    public async Task ClearRoleUsersCache(long roleId)
    {
        var userIds = await GetRoleUserIds(roleId);
        foreach (var userId in userIds)
        {
            ClearUserPermissionCache(userId);
        }
    }
}

8.3 安全审计

记录权限变更日志

/// <summary>
/// 权限变更审计
/// </summary>
public class PermissionAuditService : ITransient
{
    private readonly SqlSugarRepository<SysAuditLog> _auditRep;
    private readonly IUserManager _userManager;

    /// <summary>
    /// 记录角色权限变更
    /// </summary>
    public async Task LogRolePermissionChange(long roleId, 
        List<long> oldMenuIds, List<long> newMenuIds)
    {
        var addedMenus = newMenuIds.Except(oldMenuIds).ToList();
        var removedMenus = oldMenuIds.Except(newMenuIds).ToList();

        var log = new SysAuditLog
        {
            Module = "权限管理",
            Operation = "角色授权变更",
            OperatorId = _userManager.UserId,
            OperatorName = _userManager.RealName,
            TargetId = roleId,
            Detail = JSON.Serialize(new 
            {
                AddedMenus = addedMenus,
                RemovedMenus = removedMenus
            }),
            OperateTime = DateTime.Now
        };

        await _auditRep.InsertAsync(log);
    }

    /// <summary>
    /// 记录用户角色变更
    /// </summary>
    public async Task LogUserRoleChange(long userId, 
        List<long> oldRoleIds, List<long> newRoleIds)
    {
        var addedRoles = newRoleIds.Except(oldRoleIds).ToList();
        var removedRoles = oldRoleIds.Except(newRoleIds).ToList();

        var log = new SysAuditLog
        {
            Module = "权限管理",
            Operation = "用户角色变更",
            OperatorId = _userManager.UserId,
            OperatorName = _userManager.RealName,
            TargetId = userId,
            Detail = JSON.Serialize(new 
            {
                AddedRoles = addedRoles,
                RemovedRoles = removedRoles
            }),
            OperateTime = DateTime.Now
        };

        await _auditRep.InsertAsync(log);
    }
}

8.4 常见问题处理

问题1:菜单权限不生效

排查步骤:

  1. 检查用户是否分配了角色
  2. 检查角色是否分配了菜单权限
  3. 检查缓存是否已更新
  4. 检查前端路由配置
// 调试工具:检查用户完整权限链
public async Task<UserPermissionDebug> DebugUserPermission(long userId)
{
    return new UserPermissionDebug
    {
        UserId = userId,
        Roles = await GetUserRoles(userId),
        MenuIds = await GetUserMenuIds(userId),
        Permissions = await GetUserPermissions(userId),
        DataScope = await GetUserDataScope(userId),
        CacheStatus = CheckCacheStatus(userId)
    };
}

问题2:数据权限过滤失效

排查步骤:

  1. 检查实体是否继承EntityTenant
  2. 检查是否添加了DataScopeFilter特性
  3. 检查角色的数据权限范围配置
  4. 检查SQL日志确认过滤条件
// 开启SQL日志排查
services.AddSqlSugar(config =>
{
    config.EnableSqlLog = true;
    config.SqlLogAction = (sql, pars) =>
    {
        Console.WriteLine($"SQL: {sql}");
        Console.WriteLine($"Parameters: {JSON.Serialize(pars)}");
    };
});

总结

本章详细介绍了Admin.NET的权限系统和多租户实现:

  1. RBAC权限模型:用户-角色-权限的经典模型
  2. JWT认证机制:Token生成、验证和刷新
  3. 菜单权限控制:动态菜单加载和权限过滤
  4. 按钮权限控制:前后端按钮级别的权限控制
  5. 数据权限控制:多种数据范围的细粒度控制
  6. 多租户架构:共享数据库和独立数据库模式
  7. 租户权限隔离:菜单隔离和数据隔离
  8. 最佳实践:权限设计、缓存策略、安全审计

掌握权限系统是进行业务开发的基础。在下一章中,我们将深入学习数据库操作和SqlSugar的使用。