给制药 GMP 车间做上位机,权限和审计不是"加个登录框"那么简单。审计官查的是三件事:谁在什么时间、从哪台机器、把什么东西从什么值改成了什么值,改的时候有没有合法授权和签名。我做过的冻干车间、灌装线项目里,权限这块返工最多——功能写完了,QA 一句"这条记录不能证明是谁改的",整个配方管理模块推倒重来。这篇把权限分级、电子签名、审计日志的落地要点一次说清。
先想清楚:审计日志到底要追到什么程度?
普通上位机的操作日志记个"张三登录、李四退出"就完事,合规场景远远不够。GMP(以及出口时的 FDA 21 CFR Part 11)要求每条审计记录能回答五个问题,缺一个都可能在检查时被开缺陷项:
另外两类动作必须强制填操作原因/备注:偏差处理(比如跳过某个联锁报警继续生产)、超限参数修改。原因不能是下拉框随便选一个,要有自由文本栏,这是审计时判断"操作是否经过思考"的依据。
角色怎么分:权限跟着岗位职责走,不跟着人走
最忌讳的设计是"给每个人单独勾权限"。正确做法是 RBAC——账号绑角色,角色绑权限,人换岗只换角色,权限矩阵不动。制药车间常见五类角色,权限颗粒度落到"功能 + 数据范围":
| 功能 / 角色 | 操作员 | 班长 | 工艺工程师 | 设备管理员 | QA 审计员 |
|---|---|---|---|---|---|
| 实时监控 / 报警确认 | ✓ | ✓ | ✓ | ✓ | 只读 |
| 启停设备 / 调用配方 | ✓ | ✓ | — | — | — |
| 报警偏差放行(继续生产) | — | 签名+原因 | — | — | — |
| 配方参数修改 | — | — | 双签生效 | — | — |
| 用户账号管理 | — | — | — | ✓ | — |
| 审计日志查询/导出 | — | — | — | — | ✓ |
电子签名不是"再弹一个密码框"
很多人理解的电子签名,是点"确定"时再输一次密码。差得远。Part 11 对电子签名的要求核心是不可抵赖,落地上有四个硬点:
- 签名必须重新认证身份:即使你已登录,签名动作要再次输入账号密码(或刷工牌/指纹),证明"此刻坐在这里的确实是本人",不能把登录态当签名;
- 签名要带"含义声明":签名框上必须明确写"我确认本次操作表示:审核批准 / 已复核无误",签名人签的是一个具体含义,不是糊里糊涂点确定;
- 签名记录和业务记录永久绑定:谁签的、什么时间签的、签的是哪条数据、签名含义,全部进审计日志,签名后数据锁定不可改,要改只能走"作废重签"流程;
- 关键动作双签:配方参数修改这类影响产品质量的操作,工艺工程师签"提交"、QA 或第二人签"复核批准",两个签名都到位才生效。单人签了不生效,这叫双人复核(four-eyes principle)。
审计日志怎么存,才防得住"删库改记录"?
日志表如果和业务表在同一个库、同一个账号能 delete,审计就是纸糊的。我们的做法有三层:
- 只追加,不修改不删除:日志表只给程序账号 INSERT 权限,不给 UPDATE/DELETE;数据库层面用触发器或权限回收把死路堵上,连 DBA 直连改库都留痕;
- 哈希链防篡改:每条日志存前一条记录的哈希值,新记录哈希 = Hash(本条内容 + 上一条哈希)。任何一条被改过,后面整条链对不上,导出审计时一键校验全链完整性;
- 时间可信:上位机开机强制 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 拉进来,让他们对着权限矩阵和审计样例签字确认,别等开发完了再补——补的时候你会发现,"前后值记录"这种需求要改的是整个数据访问层。今天就可以做一件事:翻出你现在系统的操作日志表,看看有没有"改前值"这一列。没有的话,这就是最大的合规窟窿。
