当前位置: 首页 > news >正文

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

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

目录

  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. 生成Tokenvar 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){// 验证RefreshTokenvar 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);// 生成新Tokenvar 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;}// 自动刷新Tokenif (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext())){await AuthorizeHandleAsync(context);}else{context.Fail();}}/// <summary>/// 授权处理/// </summary>public override async Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext){// 获取用户Idvar 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;// 获取当前租户Idvar 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的使用。


← 上一章目录
http://www.jsqmd.com/news/1051695/

相关文章:

  • CompressO终极指南:免费开源的视频图像压缩神器
  • 家里管道堵了别乱找!2026深圳正规疏通维修团队甄选指南 - 宅安选房屋修缮
  • 家里管道堵了别乱找!2026天津正规疏通维修团队甄选指南 - 宅安选房屋修缮
  • emWin控件实战:MULTIPAGE、PROGBAR、RADIO、SCROLLBAR核心API与嵌入式GUI开发指南
  • 第二章:安装与环境配置
  • 基于OpenCV和Python的人脸识别系统_django2(设计源文件+万字报告+讲解)(支持资料、图片参考_降重降ai)
  • 2026年中四川地区老旧房改造诚信深度解析与推荐 - 品牌鉴赏官2026
  • 2026六盘水漏水检测维修本地口碑防水商家榜单:厨卫/阳台/屋面/地下室渗漏水维修,持证施工+明码实价,防水补漏公司TOP5推荐 - 即刻修防水
  • 第五章:数据库操作与SqlSugar集成
  • 家里管道堵了别乱找!2026上海正规疏通维修团队甄选指南 - 宅安选房屋修缮
  • 渗透工程师成长路线:从Web安全到内网渗透的实战进阶指南
  • 架构重构指南:PCL2启动器Java环境管理的三层架构深度解析
  • 家里管道堵了别乱找!2026广州正规疏通维修团队甄选指南 - 宅安选房屋修缮
  • 终极指南:3步掌握SMUDebugTool,轻松优化AMD Ryzen系统性能
  • 机房电源线老化必须换整条吗
  • AutoSubs终极指南:如何在本地设备上免费生成AI字幕,无缝集成DaVinci Resolve
  • 如何5分钟搭建你的私人游戏云:Sunshine跨平台串流终极指南
  • emWin LISTVIEW控件详解:从基础创建到高级定制实战
  • 如何为欧洲卡车模拟2快速配置智能驾驶辅助:终极指南
  • emWin GUI开发实战:API故障排查与性能优化全流程解析
  • 嵌入式GUI开发实战:从零配置emWin图形库到Hello World显示
  • 2026 AI Skills仓库实战指南:可用性、可维护性与可组合性
  • 网盘直链下载助手终极指南:八大主流网盘全速下载解决方案
  • Windows本地AI工作流部署:OpenClaw+Redis+PowerShell环境契约式配置
  • 如何彻底解决Windows C盘爆红问题:终极清理工具使用指南
  • 终极指南:如何通过FanControl实现Windows系统风扇精准控制与静音优化
  • p056基于spark的短视频推荐系统的设计与实现1(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_可以扫码
  • NXP嵌入式平台GPU驱动配置与Wayland图形系统实战指南
  • 2026年新发布GEO怎么联系?资深分析师解读联系与选型关键路径 - 品牌鉴赏官2026
  • 2026年新消息:广州知名灌浆料供应商选型指南与亚成新材料深度解析 - 品牌鉴赏官2026