本文描述了一个匿名化的企业级实施案例。公司名称、内部域名、仓库标识符、工单编号以及专有控制名称均已被有意删除或泛化。
多因素身份验证通常被描述为一个登录问题:输入密码、接收验证码、确认身份。
对于本案例研究中描述的系统,这种模式还不够。
该产品运行在店内环境中,同一台平板电脑可能在一次交易中被多人使用:
- 一名员工发起流程;
- 一名经理审批或支持;
- 一名顾客在自己的设备上查看并签署。
挑战不仅仅是证明用户知道一个六位数代码。我们需要创建一个安全、短期的交接机制,在共享的店内会话与顾客的个人手机之间进行,同时不泄露后续的签名会话,也不允许多个设备认领同一笔交易。
本文将解释该平台背后的架构、安全模型、权衡以及生产实践。
实际问题:安全设备交接
工作流从一台共享平板电脑开始。在某一时刻,顾客需要将部分流程继续到自己的手机上。
因此平台必须回答几个问题:
- 手机如何证明它属于站在员工面前的顾客?
- 共享平板如何知道正确的手机认领了正确的会话?
- 如果二维码被扫描两次会发生什么?
- 我们如何防止会话标识符和令牌出现在 URL、浏览器历史、日志或引用头中?
- 验证成功后,我们如何立即通知手机?
- 我们如何确保一次性签名 URL 在验证完成前不被暴露?
这些约束条件将一个看似简单的 MFA 功能转变为一个分布式系统问题,涉及身份、实时通信、并发、边缘交付、基础设施和运维控制。
高层架构
解决方案使用了两个主要应用组件:
- 面向顾客手机的 React 和 TypeScript 单页应用;
- 基于 AWS AppSync、Lambda 和 DynamoDB 构建的无服务器 GraphQL API。
基础设施还包括 CloudFront、S3、API Gateway、Route 53、ACM、WAF、Terraform、集中式日志、APM、合成监控以及自动化部署工作流。
┌─────────────────────────────┐
│ Shared in-store tablet │
│ Employee / manager workflow │
└──────────────┬──────────────┘
│ create handoff session
▼
┌─────────────────────────────┐
│ GraphQL API │
│ AppSync + Lambda │
└──────────────┬──────────────┘
│ persist state
▼
┌─────────────────────────────┐
│ DynamoDB │
│ session + short-link data │
└─────────────────────────────┘
Shared tablet displays QR
│
▼
┌─────────────────────────────┐
│ Customer phone │
│ React SPA via CloudFront │
└──────────────┬──────────────┘
│ claim + subscribe
▼
┌─────────────────────────────┐
│ GraphQL API │
│ verification + status push │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode
该架构有意采用无服务器设计。工作负载具有突发性,用户旅程较短,大多数操作都是小型状态转换而非长时间运行的计算。
端到端会话交接流程
1. 创建交接会话
店内应用创建了一条短期的交接记录,包含交易上下文、下游签名 URL、过期数据以及 PENDING 状态。
签名 URL 被视为敏感且一次性的。它被内部存储,但在交接会话仍处于待定状态时,从不返回给面向顾客的应用。
2. 生成短 QR 链接
API 创建了一个与交接记录关联的短代码。店内平板将该短链接渲染为二维码。
顾客用手机扫描该二维码。
3. 安全引导手机会话
QR 请求通过 CloudFront 和一个小重定向端点。
重定向没有将会话标识符和令牌放在查询参数中,而是返回配置了以下属性的短期 Cookie:
-
Secure; -
SameSite=Strict; - 非常短的过期时间。
手机应用读取这些值,配置其 GraphQL 客户端,并继续会话交接流程。
这避免了通过以下方式暴露凭证:
- 浏览器历史;
- 访问日志;
- 复制的 URL;
- 分析工具;
- 引用头。
4. 认领交接会话
手机调用 claimSession 变更。
后端生成:
- 一个六位数安全码;
- 一个唯一的会话标识符。
该代码显示在顾客手机上,并通过共享平板读回给员工。
平台采用最后扫描者获胜模型。如果另一部手机扫描相同的二维码,新的认领将取代之前的认领。
这防止了两台设备持有同一活跃交接会话。
5. 验证代码
员工在店内应用中输入六位数代码。
后端将验证评估为状态转换:
- 代码正确:
PENDING → VERIFIED; - 代码错误且仍有尝试次数:减少尝试次数;
- 最后一次错误尝试:
PENDING → CANCELED; - 超时:
PENDING → EXPIRED。
这些转换使用 DynamoDB 条件写入而非读-修改-写序列。数据库成为并发原语。
这一点很重要,因为两个验证请求或两个竞争认领不能同时成功。
6. 实时推送结果
手机使用 GraphQL 订阅订阅交接状态更新。
当验证成功时,AppSync 立即将新状态推送到手机。
没有轮询循环。
手机随后执行最后一次经过身份验证的读取。仅在此时,API 才暴露一次性签名 URL。
应用将顾客重定向到签名体验并使交接记录过期。
为什么使用 GraphQL 订阅而不是轮询?
轮询更容易解释,但也会引入不必要的流量、更慢的反馈以及更多的生命周期边缘情况。
产品需求本质上是事件驱动的:
当店内会话验证了这个确切的交接会话时,通知这部手机。
GraphQL 订阅提供了:
- 低延迟的状态变更;
- 按交接上下文内置过滤;
- 通过客户端库管理的 WebSocket 重连;
- 无需自定义轮询间隔或重试调度器。
前端仍需处理一种移动设备特有的故障模式:当用户锁屏或切换应用时,操作系统经常会挂起浏览器标签页。
当标签页再次可见时,应用会重新获取交接状态。这覆盖了 WebSocket 被暂停且错过最终事件的情况。
六位数代码的两个用途
最重要的设计决策之一是将六位数值同时用作:
- 人类可读的 MFA 挑战;
- 访问交接状态所需的服务器端能力。
该代码在读取和订阅过滤时都需要。它不会返回给已被取代的设备,且在状态达到 VERIFIED 之前,下游签名 URL 始终保持隐藏。
这消除了对第二个不透明会话令牌的需求,同时保留了字段级授权。
它还减少了需要生成、存储、同步和失效的凭证数量。
分层安全模型
平台采用纵深防御,而不是将 MFA 代码视为唯一的控制手段。
边缘保护
CloudFront 和区域端点受 AWS WAF 保护。托管规则组分阶段引入,高风险规则优先执行,而易产生误报的规则组最初保持在监控模式。
传输与身份
所有流量均使用 HTTPS。GraphQL API 使用自定义 Lambda 授权器,验证 OAuth/JWT 令牌、签发者、签名、过期时间、操作级作用域和授权权重。
未知操作将失败关闭。
契约强化
GraphQL 接口在已部署环境中禁用了内省,并限制了查询深度和解析器数量。
字段级授权
解析器在返回交接状态前检查安全码。
仅当交接会话已验证时才包含签名 URL。
对于已被取代的订阅,解析器不返回数据,而不是揭示会话变更的原因。
凭证卫生
敏感的交接凭证不通过 URL 传递。日志排除个人身份信息和身份验证密钥。保留关联标识符以便调试,但不泄露会话数据。
保持前端有意精简
面向顾客的应用是一个单一用途流程,因此前端避免了不必要的框架复杂度。
它使用了:
- React;
- 严格模式的 TypeScript;
- Vite;
- React Context 和本地钩子;
- 托管的 GraphQL 客户端;
- 共享的企业设计系统。
没有路由器,也没有全局状态库。
应用只有几个状态:
- 加载或认领中;
- 显示 MFA 代码;
- 显示错误或终端状态;
- 验证后重定向。
这种简洁性减少了包体积、攻击面、升级负担和认知开销。
从第一天起即采用基础设施即代码
所有基础设施均用 Terraform 定义。
API 仓库不仅配置了后端,还配置了前端所需的共享基础设施:
- AppSync;
- Lambda 函数;
- DynamoDB;
- S3;
- CloudFront;
- API Gateway;
- DNS 和证书;
- WAF;
- 监控资源。
前端仓库生成不可变的构建产物,但不拥有单独的基础设施栈。
这为平台拓扑创建了单一事实来源,同时允许应用和 API 独立演进。
一次构建,多处部署
前端没有在构建时嵌入环境特定配置。
相反,它从部署的主机名派生环境。因此同一构建产物可以原样提升通过开发、QA、预生产和生产环境。
该模式提供了两个好处:
- 生产环境收到之前已测试过的完全相同的产物;
- 配置仍是部署关注点而非构建关注点。
交付管道包括:
- 代码检查和自动化测试;
- 静态分析;
- 基础设施验证;
- 安全扫描;
- 语义版本控制;
- 不可变产物发布;
- 环境审批;
- 生产变更管理集成。
可观测性是产品的一部分
运维可见性并非在上线后才添加。
平台包含:
- 结构化 JSON 日志;
- 关联标识符;
- 命名业务事件;
- 集中式日志转发;
- APM 插桩;
- 针对 Lambda、GraphQL 和交易健康的仪表盘;
- 延迟、错误率和限流告警;
- 合成健康检查;
- 环境特定的升级规则。
监控配置位于独立 Terraform 状态中,因此可以在不修改主应用基础设施的情况下更改。
这种分离很有用,因为告警通常以不同于应用资源的节奏演进。
真实权衡:合规优先于更便宜的 API 选项
涉及 API Gateway 的决策更具启发性。
第一个实现使用了较新的 HTTP API,因为它更简单且更便宜。在合规工作中,团队确认该 API 类型不支持所需的 WAF 关联。
可用选项包括:
- 保留较新的 API 并申请例外;
- 重新设计边缘流程;
- 迁移到支持所需 WAF 附件的 REST API v1。
平台迁移到了 REST API v1。
这增加了每次请求的成本,但观察到的流量足够低,每月影响可以忽略不计。安全和合规收益比微小的基础设施节省更重要。
教训并非 REST API v1 总是更好,而是要根据完整的生产约束集(而非仅开发便利性或标价)评估云服务选择。
按垂直切片交付
产品通过增量阶段从空仓库走向生产:
- 基础 API、数据库和 Terraform 模块;
- 边缘交付、DNS、证书和首个移动 UI;
- 自定义授权和作用域访问;
- 过期、短链接、QR 重定向和安全会话引导;
- 独占认领和实时订阅;
- 结构化日志、仪表盘、告警和合成监控;
- WAF 强制执行和合规 rollout;
- 生产自动化和生命周期维护。
每个阶段交付一个完整的架构能力,而不是一组不连贯的文件。
这使集成风险更早可见,并允许设计随着平台约束的发现而演进。
值得复用的模式
对小型分布式状态机使用条件写入
对于具有简单转换的工作流,DynamoDB 条件表达式可以取代应用锁和先读后写逻辑。
将被拒绝的设计视为有价值的文档
若干实现路径因平台约束而失败。记录失败原因可防止未来工程师重复相同的实验。
尽可能将授权推近数据
解析器中的字段级检查确保查询和订阅遵循相同规则。
逐步发布安全控制
WAF 配置以监控模式引入、分析,然后通过回滚标志和生产金丝雀选择性强制执行。
保持单一用途前端的单一用途
缺少路由器或复杂状态库是经过深思熟虑的架构选择,而非缺少复杂性。
在初始架构中构建生产就绪性
身份验证、CI/CD、可观测性、安全扫描、健康检查和回滚机制不应推迟到功能完成后。
最终要点
该平台最难的部分并非生成六位数代码。
真正的工程挑战是在处理并发、实时状态、短期凭证、一次性下游会话、生产安全和企业交付控制的同时,在共享设备与顾客自有设备之间建立可信的交接。
当自定义 MFA 流程被视为一个完整的分布式系统而非小型身份验证组件时,它才具有价值。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.