第四章:权限系统与多租户实现
目录
- RBAC权限模型概述
- 用户认证机制
- 菜单权限控制
- 按钮权限控制
- 数据权限控制
- 多租户架构设计
- 租户权限隔离
- 权限相关最佳实践
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:菜单权限不生效
排查步骤:
- 检查用户是否分配了角色
- 检查角色是否分配了菜单权限
- 检查缓存是否已更新
- 检查前端路由配置
// 调试工具:检查用户完整权限链
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:数据权限过滤失效
排查步骤:
- 检查实体是否继承EntityTenant
- 检查是否添加了DataScopeFilter特性
- 检查角色的数据权限范围配置
- 检查SQL日志确认过滤条件
// 开启SQL日志排查
services.AddSqlSugar(config =>
{config.EnableSqlLog = true;config.SqlLogAction = (sql, pars) =>{Console.WriteLine($"SQL: {sql}");Console.WriteLine($"Parameters: {JSON.Serialize(pars)}");};
});
总结
本章详细介绍了Admin.NET的权限系统和多租户实现:
- RBAC权限模型:用户-角色-权限的经典模型
- JWT认证机制:Token生成、验证和刷新
- 菜单权限控制:动态菜单加载和权限过滤
- 按钮权限控制:前后端按钮级别的权限控制
- 数据权限控制:多种数据范围的细粒度控制
- 多租户架构:共享数据库和独立数据库模式
- 租户权限隔离:菜单隔离和数据隔离
- 最佳实践:权限设计、缓存策略、安全审计
掌握权限系统是进行业务开发的基础。在下一章中,我们将深入学习数据库操作和SqlSugar的使用。
← 上一章目录
