惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
MongoDB | Blog
MongoDB | Blog
Engineering at Meta
Engineering at Meta
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
T
The Exploit Database - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
月光博客
月光博客
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
NISL@THU
NISL@THU
爱范儿
爱范儿
S
Securelist
博客园 - 叶小钗
C
CERT Recently Published Vulnerability Notes
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
D
DataBreaches.Net
G
GRAHAM CLULEY
P
Proofpoint News Feed
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
C
Cyber Attacks, Cyber Crime and Cyber Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Recent Announcements
Recent Announcements
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
Jina AI
Jina AI
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
博客园 - 【当耐特】
H
Help Net Security
F
Fortinet All Blogs
T
The Blog of Author Tim Ferriss

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第四章:权限系统与多租户实现
我才是银古 · 2026-06-21 · via 博客园 - 我才是银古

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

目录

  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支持多种多租户模式:

共享数据库模式(默认)

  • 所有租户共用一个数据库
  • 通过TenantId字段区分数据
  • 实现简单,资源利用率高

独立数据库模式

  • 每个租户使用独立的数据库
  • 数据完全隔离
  • 适合对安全性要求高的场景

混合模式

  • 核心数据共享
  • 业务数据隔离
  • 兼顾效率和安全

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的使用。