从零搭建工业控制系统(十):权限管理——不是所有按钮都能点

权限管理:不是所有按钮都能点

这是「从零搭建工业控制系统」系列第10篇。上篇聊完报警系统,这篇说权限——谁能让设备动起来,谁只能看着。


一场事故引出的权限设计

去年现场出过一次事故。夜班操作员想调加热器温度,误触了"原始IO调试"面板,直接写了一个0到温度寄存器。加热器瞬间断电,正在跑的工艺晶圆报废。

事后复盘,问题不在于操作员手滑,而在于系统不该让操作员看到那个面板

从那以后我做了权限系统。核心原则就一条:界面只展示你有权操作的东西,没权限的按钮直接不显示。


三级角色模型

项目里定义了三个角色,权限逐级递增:

public enum UserRole
{
    Operator = 0,   // 操作员:只能跑工艺、看日志
    Engineer = 1,   // 工程师:能调参数、手动控制、编辑配方
    Admin = 2       // 管理员:能调试原始IO、管理用户
}

角色之间是包含关系。Engineer拥有Operator的全部权限再加自己的,Admin拥有Engineer的全部再加自己的。这样设计的好处是权限只增不减,不用每次都从头定义。

图1:三级角色权限包含关系


权限映射表

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? 因为禁用的按钮虽然不能点,但操作员能看到"有这个功能"。看不到比看得到点不了更安全——你不知道的东西不会想去碰。

图3:UI元素按权限显隐对照

图4:Visibility vs 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;

角色一变,所有绑定了 IsOperatorIsEngineerOrAboveIsAdmin 的UI元素自动更新显隐。不需要手动刷新。

图2:登录认证与权限刷新流程


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篇:阀门控制系统实战

权限管好了,接下来看怎么安全地控制阀门——预检查、联锁、组合命令、状态等待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值