给制药 GMP 车间做上位机,权限和审计不是"加个登录框"那么简单。审计官查的是三件事:谁在什么时间、从哪台机器、把什么东西从什么值改成了什么值,改的时候有没有合法授权和签名。我做过的冻干车间、灌装线项目里,权限这块返工最多——功能写完了,QA 一句"这条记录不能证明是谁改的",整个配方管理模块推倒重来。这篇把权限分级、电子签名、审计日志的落地要点一次说清。

先想清楚:审计日志到底要追到什么程度?

普通上位机的操作日志记个"张三登录、李四退出"就完事,合规场景远远不够。GMP(以及出口时的 FDA 21 CFR Part 11)要求每条审计记录能回答五个问题,缺一个都可能在检查时被开缺陷项:

Who 谁操作人唯一账号,实名,不许共用账号、不许通用密码
When 何时带时间戳,工控机时间统一走 NTP 对时,不能本机随便改
Where 在哪操作终端/工位标识,远程操作要记来源 IP
What 做了什么动作类型 + 对象,如"修改配方 RC-2026 灭菌段温度设定"
前后值改前值、改后值都要落库,光记"修改了配方"等于没记

另外两类动作必须强制填操作原因/备注:偏差处理(比如跳过某个联锁报警继续生产)、超限参数修改。原因不能是下拉框随便选一个,要有自由文本栏,这是审计时判断"操作是否经过思考"的依据。

角色怎么分:权限跟着岗位职责走,不跟着人走

最忌讳的设计是"给每个人单独勾权限"。正确做法是 RBAC——账号绑角色,角色绑权限,人换岗只换角色,权限矩阵不动。制药车间常见五类角色,权限颗粒度落到"功能 + 数据范围":

功能 / 角色操作员班长工艺工程师设备管理员QA 审计员
实时监控 / 报警确认只读
启停设备 / 调用配方
报警偏差放行(继续生产)签名+原因
配方参数修改双签生效
用户账号管理
审计日志查询/导出
一条红线:开发和运维账号不能出现在生产系统里。设备管理员能建账号、改配置,但不能签配方、不能放行偏差;QA 只有看日志的权限,没有任何操作权限——"干活的人不能管账,管账的人不能干活",职责分离(SoD)是审计必查项。我们项目交付前会用一张矩阵表过一遍,凡是出现"全能账号"一律打回。

电子签名不是"再弹一个密码框"

很多人理解的电子签名,是点"确定"时再输一次密码。差得远。Part 11 对电子签名的要求核心是不可抵赖,落地上有四个硬点:

  • 签名必须重新认证身份:即使你已登录,签名动作要再次输入账号密码(或刷工牌/指纹),证明"此刻坐在这里的确实是本人",不能把登录态当签名;
  • 签名要带"含义声明":签名框上必须明确写"我确认本次操作表示:审核批准 / 已复核无误",签名人签的是一个具体含义,不是糊里糊涂点确定;
  • 签名记录和业务记录永久绑定:谁签的、什么时间签的、签的是哪条数据、签名含义,全部进审计日志,签名后数据锁定不可改,要改只能走"作废重签"流程;
  • 关键动作双签:配方参数修改这类影响产品质量的操作,工艺工程师签"提交"、QA 或第二人签"复核批准",两个签名都到位才生效。单人签了不生效,这叫双人复核(four-eyes principle)。

审计日志怎么存,才防得住"删库改记录"?

日志表如果和业务表在同一个库、同一个账号能 delete,审计就是纸糊的。我们的做法有三层:

  1. 只追加,不修改不删除:日志表只给程序账号 INSERT 权限,不给 UPDATE/DELETE;数据库层面用触发器或权限回收把死路堵上,连 DBA 直连改库都留痕;
  2. 哈希链防篡改:每条日志存前一条记录的哈希值,新记录哈希 = Hash(本条内容 + 上一条哈希)。任何一条被改过,后面整条链对不上,导出审计时一键校验全链完整性;
  3. 时间可信:上位机开机强制 NTP 对时,对时失败弹警示;日志时间戳取服务器时间而非客户端本地时间,防止有人改本机时钟伪造操作时间。
// ① 权限校验:用特性把权限要求声明在动作上,拦截器统一检查
[RequireRole(Role.ProcessEngineer)]
[RequireSignature(Meaning = "配方修改提交")]
public ResultDto SubmitRecipe(RecipeDto dto)
{
    var old = _recipeRepo.Get(dto.Id);
    var result = _recipeRepo.Update(dto);   // 业务先落库

    // ② 审计日志:前后值 + 操作人 + 原因,一条都不能少
    _audit.Write(new AuditEntry {
        Action      = "RECIPE_MODIFY",
        Target      = $"配方 {dto.Code} 灭菌段设定温度",
        OldValue    = old.SterilTemp.ToString(),
        NewValue    = dto.SterilTemp.ToString(),
        Reason      = dto.ChangeReason,          // 必填,前端拦截空值
        Signer      = CurrentUser.Id,
        SignatureId = CurrentSignature.Id,        // 绑定本次电子签名记录
        Workstation = Environment.MachineName,
        ClientIp    = CurrentClient.Ip
    });
    return result;
}

// ③ 哈希链:每条日志咬住前一条,改一条全链作废
public void AppendChain(AuditEntry e)
{
    var prev = _db.AuditLogs.OrderByDescending(a => a.Seq).FirstOrDefault();
    e.PrevHash = prev?.ChainHash ?? "";
    e.ChainHash = Sha256($"{e.Seq}|{e.Action}|{e.NewValue}|{e.Signer}|{e.PrevHash}");
    _db.AuditLogs.Add(e);   // 连接串账号仅授 INSERT,无 UPDATE/DELETE 权限
    _db.SaveChanges();
}

▲ 电子签名的校验在进 Action 之前由拦截器完成:弹签名框 → 重新验证账号密码 → 生成 SignatureId → 带着签名上下文执行业务。验证失败直接拦掉,业务代码根本进不来。

交付前照着过:合规自检清单

  • 账号唯一:没有任何共用账号、通用密码,离职账号 24 小时内停用,账号清单与人事名册一致
  • 密码策略:首次登录强制改密、90 天过期、错误 5 次锁定 30 分钟、密码不明文存储(加盐哈希)
  • 会话超时:无操作 10~15 分钟自动锁屏,回操作界面需重新登录
  • 关键动作双签:配方修改、偏差放行、超限参数全部强制电子签名,双签缺一不生效
  • 审计五要素齐全:抽查 10 条记录,谁/何时/哪台机/做了什么/前后值都能对上
  • 日志防篡改:程序账号无删改权限,哈希链校验通过,日志保留期满足文件归档年限(通常不少于产品有效期后 1 年)
  • 时间可信:NTP 对时正常,断网对时失败有醒目警示且记入日志
  • 职责分离:不存在"全能账号",管账号的人不签配方、QA 只读不操作

最后一句实话

权限审计模块是上位机里最没"技术炫技"空间、却最容易返工的部分。我的建议是:项目需求评审阶段就把 QA 拉进来,让他们对着权限矩阵和审计样例签字确认,别等开发完了再补——补的时候你会发现,"前后值记录"这种需求要改的是整个数据访问层。今天就可以做一件事:翻出你现在系统的操作日志表,看看有没有"改前值"这一列。没有的话,这就是最大的合规窟窿。