权限管理:不是所有按钮都能点
这是「从零搭建工业控制系统」系列第10篇。上篇聊完报警系统,这篇说权限——谁能让设备动起来,谁只能看着。
一场事故引出的权限设计
去年现场出过一次事故。夜班操作员想调加热器温度,误触了"原始IO调试"面板,直接写了一个0到温度寄存器。加热器瞬间断电,正在跑的工艺晶圆报废。
事后复盘,问题不在于操作员手滑,而在于系统不该让操作员看到那个面板。
从那以后我做了权限系统。核心原则就一条:界面只展示你有权操作的东西,没权限的按钮直接不显示。
三级角色模型
项目里定义了三个角色,权限逐级递增:
public enum UserRole
{
Operator = 0, // 操作员:只能跑工艺、看日志
Engineer = 1, // 工程师:能调参数、手动控制、编辑配方
Admin = 2 // 管理员:能调试原始IO、管理用户
}
角色之间是包含关系。Engineer拥有Operator的全部权限再加自己的,Admin拥有Engineer的全部再加自己的。这样设计的好处是权限只增不减,不用每次都从头定义。

权限映射表
PermissionService 是权限系统的核心。它维护了一张角色到权限的映射表:
public class PermissionService : ObservableObject
{
private readonly Dictionary<UserRole, HashSet<string>> _rolePermissions;
private UserRole _currentRole = UserRole.Operator;
public PermissionService()
{
_rolePermissions = BuildRolePermissions();
}
private static Dictionary<UserRole, HashSet<string>> BuildRolePermissions()
{
// Operator:仅工艺执行 + 配方查看 + 日志查看
var operatorPerms = new HashSet<string>
{
UserPermission.AutoView,
UserPermission.AutoRunSequence,
UserPermission.AutoLoadUnload,
UserPermission.AutoCoverControl,
UserPermission.RecipeView,
UserPermission.LogView
};
// Engineer:Operator + 系统设置 + 手动控制 + 配方编辑
var engineerPerms = new HashSet<string>(operatorPerms)
{
UserPermission.SettingsAccess,
UserPermission.SettingsTraceView,
UserPermission.SettingsIOView,
UserPermission.SettingsHeater,
UserPermission.ManualAccess,
UserPermission.ManualMfcControl,
UserPermission.ManualValveControl,
UserPermission.RecipeEdit,
UserPermission.RecipeCreate,
UserPermission.LogExport
};
// Admin:Engineer + 原始IO调试 + 命令测试 + 用户管理
var adminPerms = new HashSet<string>(engineerPerms)
{
UserPermission.SettingsRawIO,
UserPermission.SettingsCommandTest,
UserPermission.SettingsCommandDebug,
UserPermission.AdminUserManagement
};
return new Dictionary<UserRole, HashSet<string>>
{
{ UserRole.Operator, operatorPerms },
{ UserRole.Engineer, engineerPerms },
{ UserRole.Admin, adminPerms }
};
}
}
权限用字符串常量定义在 UserPermission 类里。每个权限对应一个具体功能模块,比如 ManualValveControl 是手动阀门控制,SettingsRawIO 是原始IO调试面板。
可绑定属性:XAML直接用
PermissionService 继承自 ObservableObject,角色变化时自动通知UI。几个便捷属性直接在XAML里绑定:
public bool IsOperator => CurrentRole == UserRole.Operator;
public bool IsEngineerOrAbove => CurrentRole >= UserRole.Engineer;
public bool IsAdmin => CurrentRole == UserRole.Admin;
public string UserDisplayText => $"{CurrentUsername} ({CurrentRole})";
角色切换时触发属性更新:
public UserRole CurrentRole
{
get => _currentRole;
set
{
if (SetProperty(ref _currentRole, value))
{
OnPropertyChanged(nameof(IsOperator));
OnPropertyChanged(nameof(IsEngineerOrAbove));
OnPropertyChanged(nameof(IsAdmin));
OnPropertyChanged(nameof(UserDisplayText));
_logger.Info($"当前角色变更为: {value}");
}
}
}
XAML里的权限控制
界面元素根据角色显隐,不需要在code-behind里写if-else:
<!-- 操作员可见:工艺执行面板 -->
<StackPanel Visibility="{Binding PermissionService.IsOperator, Converter={StaticResource BoolToVisibilityConverter}}">
<Button Content="启动Sequence" Command="{Binding StartSequenceCommand}" />
</StackPanel>
<!-- 工程师及以上可见:手动控制面板 -->
<StackPanel Visibility="{Binding PermissionService.IsEngineerOrAbove, Converter={StaticResource BoolToVisibilityConverter}}">
<Button Content="手动开阀门" Command="{Binding ManualValveCommand}" />
</StackPanel>
<!-- 仅管理员可见:原始IO调试 -->
<StackPanel Visibility="{Binding PermissionService.IsAdmin, Converter={StaticResource BoolToVisibilityConverter}}">
<Button Content="原始IO写入" Command="{Binding RawIOWriteCommand}" />
</StackPanel>
为什么用Visibility而不是IsEnabled? 因为禁用的按钮虽然不能点,但操作员能看到"有这个功能"。看不到比看得到点不了更安全——你不知道的东西不会想去碰。


登录流程
权限的起点是登录。IUserAuthService 定义了认证接口:
public interface IUserAuthService
{
UserAccount? CurrentUser { get; }
Task<AuthResult> LoginAsync(string userName, string password);
}
登录成功后,PermissionService 的角色和用户名被更新:
// 登录成功后
_permissionService.CurrentUsername = user.UserName;
_permissionService.CurrentRole = (UserRole)user.Role;
角色一变,所有绑定了 IsOperator、IsEngineerOrAbove、IsAdmin 的UI元素自动更新显隐。不需要手动刷新。

DI注册
PermissionService 注册为单例,因为整个应用生命周期只有一个当前用户角色:
containerRegistry.RegisterSingleton<IUserAuthService, UserAuthService>();
containerRegistry.RegisterSingleton<PermissionService>();
ViewModel通过构造函数注入:
public MainWindowViewModel(
IDeviceCmdService deviceCmdService,
IRegionManager regionManager,
PermissionService permissionService,
ExecutionProgressService executionProgress)
{
_permissionService = permissionService;
// ...
}
混淆保护
权限服务的属性名在XAML里被直接引用,混淆器不能重命名:
[System.Reflection.Obfuscation(Exclude = true, ApplyToMembers = true)]
public class PermissionService : ObservableObject
这个注解确保Release模式下混淆后,IsEngineerOrAbove 这个属性名不变。不然XAML绑定路径找不到属性,界面全白了。
权限踩坑清单
| 坑 | 现象 | 解决 |
|---|---|---|
| 用IsEnabled不用Visibility | 操作员看到禁用按钮想方设法去点 | 直接不显示 |
| 权限检查只在UI层 | 绕过UI直接调服务层 | 服务层也要检查HasPermission |
| 角色用字符串 | 大小写写错权限失效 | 用枚举UserRole |
| 混淆后属性名变了 | Release版绑定失效 | Obfuscation排除 |
| 登录后不刷新权限 | 切换账号界面没变 | ObservableObject自动通知 |
本篇小结
| 知识点 | 关键做法 |
|---|---|
| 三级角色 | Operator/Engineer/Admin,权限逐级递增 |
| 权限映射 | Dictionary<UserRole, HashSet> |
| UI控制 | Visibility绑定 + BoolToVisibilityConverter |
| 角色属性 | IsOperator/IsEngineerOrAbove/IsAdmin 便捷属性 |
| 单例服务 | PermissionService注册为Singleton |
| 混淆保护 | Obfuscation(Exclude=true) 保护绑定路径 |
权限系统的核心不是"能不能点",而是"看不看得到"。看不到的按钮不会被误触。
下期预告
第11篇:阀门控制系统实战
权限管好了,接下来看怎么安全地控制阀门——预检查、联锁、组合命令、状态等待。
:权限管理——不是所有按钮都能点&spm=1001.2101.3001.5002&articleId=163967015&d=1&t=3&u=fdee3705fa974920866ab9c6101a18d6)
379

被折叠的 条评论
为什么被折叠?



