金融级前端安全防护体系:从字段脱敏到全链路审计的落地路径
金融级前端安全防护体系从字段脱敏到全链路审计的落地路径一、前端安全不是锦上添花一张截图引发的合规事故金融类产品的安全边界和普通电商、社交产品有本质区别。一个常见的忽视点浏览器开发者工具中的网络请求面板。在常规开发思维中HTTPS 加密即安全。但实际情况是HTTPS 保护的是传输通道保护不了浏览器端的明文暴露。具体场景用户在客服页面截图求助截图内容包含身份证号明文。内网环境下浏览器 DevTools 的 Network 面板直接展示响应 JSON 中的银行卡号。前端错误监控Sentry、Fundebug将包含敏感字段的请求体上传到第三方服务。这些场景在金融合规审计中都属于中高危项。系统性地构建前端安全防护需要覆盖三个层面展示层的脱敏渲染、传输层的字段加密、以及操作层的审计轨迹。二、展示层脱敏不止是中间打星号最常见的脱敏是手机号显示为138****1234。但在金融场景需求远比这个复杂。不同角色在不同页面看到的数据粒度不同角色客户列表页详情页导出文件客服138****1234138****1234全掩码风控审核员138****12341385555123413855551234审批后可见管理员1385555123413855551234全部明文这就需要一个可配置的脱敏策略引擎type SensitiveType | mobile | idCard | bankCard | name | address | email; type MaskRule { type: keep-head-tail | keep-head | full-mask; headLen?: number; tailLen?: number; maskChar?: string; }; const MASK_RULES: RecordSensitiveType, MaskRule { mobile: { type: keep-head-tail, headLen: 3, tailLen: 4, maskChar: * }, idCard: { type: keep-head-tail, headLen: 6, tailLen: 4, maskChar: * }, bankCard: { type: keep-head, headLen: 4, maskChar: * }, name: { type: keep-head, headLen: 1, maskChar: * }, address: { type: full-mask, maskChar: * }, email: { type: keep-head-tail, headLen: 3, tailLen: 4, maskChar: * }, }; interface MaskContext { role: customer-service | risk-auditor | admin; pageType: list | detail | export; } function createMaskStrategy(ctx: MaskContext): RecordSensitiveType, boolean { const visible: RecordSensitiveType, boolean { mobile: false, idCard: false, bankCard: false, name: false, address: false, email: false, }; if (ctx.role admin) { Object.keys(visible).forEach(k (visible[k as SensitiveType] true)); return visible; } if (ctx.role risk-auditor ctx.pageType detail) { visible.mobile true; visible.idCard true; visible.name true; return visible; } return visible; } function maskValue( value: string, type: SensitiveType, ctx: MaskContext ): string { const strategy createMaskStrategy(ctx); if (strategy[type]) return value; const rule MASK_RULES[type]; switch (rule.type) { case keep-head-tail: { const head value.slice(0, rule.headLen); const tail value.slice(-rule.tailLen!); const midLen value.length - rule.headLen! - rule.tailLen!; return head rule.maskChar!.repeat(midLen) tail; } case keep-head: { const head value.slice(0, rule.headLen); return head rule.maskChar!.repeat(value.length - rule.headLen!); } case full-mask: return rule.maskChar!.repeat(value.length); default: return value; } }剪贴板防护是另一个常被忽略的点。用户选中脱敏后的文本复制实际复制的是脱敏后的值。但如果页面存在隐藏的明文字段例如input[typehidden]包含完整值通过 DOM 检查仍然可能暴露。需要禁止敏感字段以任何形式包括 hidden input 和 data 属性存在于 DOM 中// 敏感字段守卫禁止特定数据落入 DOM 属性 function sanitizeDomData(data: Recordstring, unknown): Recordstring, unknown { const sensitiveFields [idCard, bankCard, password, cvv]; const sanitized { ...data }; for (const field of sensitiveFields) { if (field in sanitized) { delete sanitized[field]; } } return sanitized; } // React 场景高阶组件拦截敏感 props function withSanitizeP extends Recordstring, unknown( sensitiveKeys: string[] ): (Wrapped: React.ComponentTypeP) React.ComponentTypeP { return Wrapped { return (props: P) { const clean { ...props } as Recordstring, unknown; for (const key of sensitiveKeys) { delete clean[key]; } return React.createElement(Wrapped, clean as P); }; }; }三、传输层加固HTTPS 不是终点HTTPS 确保了传输通道的安全但请求体和响应体的明文仍然可能被以下方式泄露浏览器扩展注入的脚本拦截 fetch/XHR。前端监控 SDK 的自动 breadcrumb 将请求数据上传。Service Worker 缓存了包含敏感数据的响应。传输层的加固策略是字段级选择性加密 请求体拦截清洗// 字段级加密仅加密敏感字段非敏感字段保留明文以支持日志和调试 interface EncryptConfig { fields: string[]; algorithm: aes-256-gcm; publicKey: CryptoKey; } async function encryptPayload( data: Recordstring, unknown, config: EncryptConfig ): PromiseRecordstring, unknown { const encrypted { ...data }; for (const field of config.fields) { if (field in encrypted typeof encrypted[field] string) { const encoder new TextEncoder(); const plaintext encoder.encode(encrypted[field] as string); const iv crypto.getRandomValues(new Uint8Array(12)); const ciphertext await crypto.subtle.encrypt( { name: AES-GCM, iv }, config.publicKey, plaintext ); // 打包 iv ciphertext 为 base64 const combined new Uint8Array(iv.length ciphertext.byteLength); combined.set(iv); combined.set(new Uint8Array(ciphertext), iv.length); encrypted[field] btoa(String.fromCharCode(...combined)); } } return encrypted; } // 请求拦截器自动加密标记字段 function createSecureFetch(config: EncryptConfig): typeof fetch { return async (input: RequestInfo | URL, init?: RequestInit) { if (init?.body typeof init.body string) { try { const parsed JSON.parse(init.body); const encrypted await encryptPayload(parsed, config); init { ...init, body: JSON.stringify(encrypted) }; } catch { // 非 JSON body跳过加密 } } return fetch(input, init); }; }对于前端监控 SDK 的泄露风险需要在 breadcrumb 收集阶段拦截// 过滤发送到监控平台的敏感请求数据 const SENSITIVE_URL_PATTERNS [ /\/api\/user\/identity/, /\/api\/order\/payment/, /\/api\/risk\/assess/, ]; function sanitizeBreadcrumb(breadcrumb: { category: string; data?: Recordstring, unknown; }): void { if (breadcrumb.category fetch || breadcrumb.category xhr) { breadcrumb.data undefined; // 不记录请求体 } } // 初始化 Sentry 时配置 Sentry.init({ dsn: __DSN__, beforeBreadcrumb(breadcrumb) { sanitizeBreadcrumb(breadcrumb); return breadcrumb; }, beforeSend(event) { // 确保事件中不包含敏感字段 return event; }, });四、审计日志体系前端不可篡改的操作证伪链后端审计日志依赖数据库的 WAL 或 binlog。前端的操作审计面临不同挑战客户端环境不可信用户可能篡改或伪造日志记录。解决方案是通过后端戳 前端摘要的双向验证构建不可否认的操作链interface AuditEntry { actionId: string; // 客户端生成的 UUID userId: string; action: string; // view_idcard | export_data | approve_loan resourceId: string; // 操作的目标资源 timestamp: number; clientDigest: string; // 客户端对操作上下文的 hash serverNonce?: string; // 服务端返回的随机数用于关联验证 } async function createAuditEntry( action: string, resourceId: string, context: Recordstring, unknown ): PromiseAuditEntry { const entry: OmitAuditEntry, serverNonce { actionId: crypto.randomUUID(), userId: getCurrentUserId(), action, resourceId, timestamp: Date.now(), clientDigest: await generateDigest({ action, resourceId, context, timestamp: Date.now(), }), }; // 发送到服务端获取 nonce 回执 const response await fetch(/api/audit/log, { method: POST, body: JSON.stringify(entry), }); const { serverNonce } await response.json(); return { ...entry, serverNonce }; } async function generateDigest(data: Recordstring, unknown): Promisestring { const encoder new TextEncoder(); const content JSON.stringify(data); const hashBuffer await crypto.subtle.digest(SHA-256, encoder.encode(content)); return Array.from(new Uint8Array(hashBuffer)) .map(b b.toString(16).padStart(2, 0)) .join(); }关键原则敏感操作必须在前端触发时立即记录与后端日志形成时间戳比对。clientDigest 包含操作上下文页面 URL、表单数据摘要确保后端无法否认前端记录。serverNonce 由后端生成并返回确保前端无法在后端执行操作前伪造完整的审计记录。五、总结金融级前端安全是一个纵深防御的体系单一的安全措施无法应对多层次的威胁。核心落地方案分为三层展示层基于角色的动态脱敏策略 DOM 级数据保护确保敏感信息不泄露到屏幕截图、DevTools 和 DOM 属性中。传输层字段级 AES-GCM 加密 监控 SDK breadcrumb 过滤确保敏感数据即使被中间人获取也无法解密同时不被第三方服务意外收集。审计层客户端摘要 服务端 nonce 的双向操作证伪链构建从前端到后端不可否认的审计轨迹。每一层的实现都有明确的适用边界。加密会增加 CPU 开销脱敏会降低客服人员的操作效率审计日志会带来存储成本。需要根据产品的合规等级和用户规模做取舍但展示层的脱敏在任何金融产品中都不应省略。