标签归档:架构师

架构师必备:解决技术问题当从第一性原理开始

前两天系统陆续告警,海外服务器访问图片 CDN 经常会出现异常,连接慢,超时,或者下载到一半了失败,错误日志如下:

> Connection broken: IncompleteRead(49292 bytes read, 2611057 more expected)', 
>IncompleteRead(49292 bytes read, 2611057 more expected

当看到这些,第一个反应就是加重试和延长超时时间,有一些缓解,但是还是会出错。

当某天下午又有问题出现后,忽然想起,重试和延长超时时间并不是解决本质的问题,只是缓解了问题。

从第一性原理,尝试脱离业务查找图片 CDN 无法访问的问题,尝试发现直接访问存储的外网地址是正常的,但是通过 CDN 会有问题,ping cdn 的域名,延时还能接受。现在看只能是回源的问题,于是找云服务商来定位日志,最终的原因是回源没有带源站地址,导致海外访问受限(为什么,这点云没有做详细解释,模模糊糊说是跨境的问题,因为在国内,之前的配置是没有问题的)。

什么是第一性原理?

这个词听起来很高大上,其实道理很简单。

想象你是个外星人,刚刚来到地球,看到人类在用轮子运输货物。你不会想”轮子就应该是圆的”,而是会问”为什么需要运输?””什么形状最省力?”通过基础的物理定律,你会发现圆形确实是最优解。

这就是第一性原理的精髓——抛开所有的经验、惯例和「理所当然」,回到最基础的事实和逻辑,重新推导解决方案。

在技术领域,我们太容易被各种「最佳实践」、「业界标准」、「大厂方案」所影响。遇到问题,第一反应往往是去搜索现成的解决方案,或者套用以前的经验。这本身没错,但当这些方法都不管用时,就需要回到问题的本质了。

为什么我们需要第一性原理?

做技术久了,你会发现一个有趣的现象:很多所谓的「技术难题」,其实根本不是技术问题。

比如,系统性能差,大家就想着优化代码、加缓存、扩容。但如果从第一性原理出发,先问问「用户真正的需求是什么?」,可能会发现某些功能根本没人用,直接下线就解决了性能问题。(有些问题不用解决,解决问题本身就行)

再比如,团队总是出线上故障,常规思路是加强测试、完善监控、制定流程。但深入分析会发现,80%的故障都源于某个老旧模块,与其不断打补丁,不如花时间重构它。

技术圈有个词叫「过度工程」,说的就是用复杂的方案解决简单的问题。为什么会这样?因为我们太习惯于在既有框架内思考,忘记了问题的本质可能很简单。

技术思维的三个陷阱

在日常工作中,有三种思维陷阱特别容易让我们偏离问题的本质:

1. 经验主义陷阱

“上次遇到类似问题就是这么解决的。”

经验是财富,但也可能是枷锁。技术在变,业务在变,用户在变,昨天的解决方案未必适合今天的问题。

曾经见过一个案例,某电商网站的订单系统经常超时。后台小哥凭经验判断是数据库性能问题,花了大量时间优化 SQL、加索引、分库分表。结果呢?问题有所减轻,但是还是会出现。

后来从头分析才发现,真正的瓶颈是订单生成时要调用外部服务做各种校验和预处理,其中一个服务响应特别慢。如果一开始就从基础事实出发——「订单生成需要多长时间?时间都花在哪里?」——问题早就解决了。

2. 权威崇拜陷阱

「Google是这么做的,肯定没错。」

大厂的方案确实值得学习,但照搬往往水土不服。Google 的解决方案是基于它的业务规模、技术栈、团队能力设计的,这些前提条件你都具备吗?

记得有个创业公司,看到大厂都在搞微服务,也把自己不到 10 人的团队、日活 1 万左右的产品拆成了十几个服务。结果呢?运维成本暴增,开发效率直线下降,最后不得不重新合并服务。

3. 工具依赖陷阱

「用了最新的框架,一定能解决问题。」

技术圈总是充满各种新工具、新框架、新概念。但工具只是工具,关键是要解决什么问题。

曾经见一个项目,前端框架从 jQuery 升级到 Angular,又换成 React。每次重构都说是为了「提升开发效率」和「改善用户体验」。可实际上,用户的核心诉求——”页面加载太慢”——从来没有被真正解决过。

如果从第一性原理思考:用户要的是什么?快速加载。为什么慢?图片太大、请求太多。怎么解决?压缩图片、合并请求。这跟用什么框架其实关系不大。

当然,框架升级并不是一个一定不对的事情,技术的升级也是有必要的,但是需要看其本质是想改变什么

第一性原理的实战应用

说了这么多理论,来看看在实际技术问题中如何应用第一性原理。

案例一:神秘的数据库连接池爆满

有个项目突然开始频繁报数据库连接池满的错误。常规思路是什么?加大连接池、优化慢查询、增加数据库实例。

但我们决定从基础事实开始:

第一步:确认基础事实

  • 连接池大小:100
  • 平均活跃连接数:20-30
  • 报错时连接数:100
  • 每个请求的数据库操作:2-3次

第二步:分解问题

既然平时只用20-30个连接,为什么会突然用满100个?

通过监控发现,每天下午3点左右连接数会激增。这个时间点有什么特殊的?

第三步:追踪本质

深入代码发现,有个定时任务在下午3点执行,它会并发处理大量数据。关键是,这个任务的事务没有正确关闭,导致连接无法释放。

第四步:简单解决

修复事务管理的bug,问题彻底解决。连接池大小维持在50就足够了。

如果一开始就盲目扩大连接池,不仅掩盖了真正的问题,还会增加数据库的负担。

案例二:永远优化不完的首页

另一个经典案例是首页性能优化。产品经理总是抱怨首页加载慢,技术团队已经做了各种优化:

  • 静态资源上CDN ✓
  • 图片懒加载 ✓
  • 接口合并 ✓
  • 服务端缓存 ✓
  • 数据库索引优化 ✓

但用户还是觉得慢。怎么办?

回到基础问题:用户说的「慢」到底是什么?

通过用户访谈发现,他们说的「慢」不是首页加载慢,而是「找到想要的商品慢」。原来首页堆积了太多内容,用户需要不断滚动、寻找,体验自然不好。

真正的解决方案:

  • 简化首页内容
  • 改进搜索和分类
  • 个性化推荐

技术优化做到极致,不如产品设计的一个小改进。这就是第一性原理的力量——让你跳出技术视角,看到问题的全貌。

案例三:微服务还是单体?

这可能是近几年最容易引发争论的架构问题。很多团队一上来就要搞微服务,理由通常是:

  • 大家都在用微服务
  • 微服务更先进
  • 方便团队协作和扩展
  • 容易扩展

但如果用第一性原理思考:

基础问题是什么?

我们要构建一个满足业务需求的系统。

核心约束是什么?

  • 团队规模:5 个人还是 50 个人?
  • 业务复杂度:简单 CRUD 还是复杂业务逻辑?
  • 性能要求:日活千万还是几万或几千?
  • 开发效率:快速迭代还是稳定运行?

从零思考架构:

如果你的团队只有 5 个人,业务逻辑也不复杂,那么单体应用可能是最优选择:

  • 部署简单
  • 调试方便
  • 没有网络开销
  • 事务处理简单

随着业务增长,当单体应用真的成为瓶颈时,再考虑拆分也不迟。这时你会更清楚哪些模块需要独立,哪些可以保持在一起。

记住,微服务不是目标,解决业务问题才是。

如何培养第一性原理思维?

知道第一性原理重要是一回事,真正在工作中应用又是另一回事。这里分享一些我的实践经验:

1. 养成追问「为什么」的习惯

遇到问题不要急着动手,先问几个为什么:

  • 为什么会出现这个问题?
  • 为什么以前没有这个问题?
  • 为什么是现在出现?
  • 为什么影响的是这些用户?

每个「为什么」都可能让你更接近问题的本质。

2. 区分表象和原因

医生看病会区分症状和病因,技术问题也一样。

用户说「系统很卡」是症状,真正的原因可能是:

  • 网络延迟高
  • 服务器 CPU 占用率高
  • 数据库死锁
  • 前端渲染性能差
  • 甚至可能是用户电脑太老… (之前工业互联网创业时就遇到过这种情况)

不要被症状迷惑,要找到真正的病因。

3. 建立度量和验证机制

第一性原理强调基于事实,而不是猜测。所以要学会度量和验证:

  • 性能问题:先测量,找到瓶颈在哪
  • 稳定性问题:收集数据,分析故障模式
  • 用户体验问题:做用户调研,理解真实需求

数据会告诉你真相,而不是你的直觉。

4. 练习「从零开始」思考

定期做这样的思维练习:

「如果让我从零开始设计这个系统/解决这个问题,我会怎么做?」

这能帮你跳出现有框架的限制,找到更优解。

5. 保持谦逊和好奇

技术发展太快,昨天的真理可能今天就过时了。保持开放的心态,随时准备推翻自己的认知。

同时,不要羞于承认「我不知道」。正是这种诚实,才能让你回到基础事实,而不是基于错误的假设做决策。

什么时候用第一性原理?

需要说明的是,不是所有问题都需要用第一性原理。如果每个问题都从零开始思考,效率会非常低。

适合使用第一性原理的场景:

  1. 常规方法失效时:试过各种方案都不管用,说明可能方向就错了
  2. 面对全新问题时:没有先例可循,必须从基础原理推导
  3. 需要创新突破时:想要 10 倍改进而不是 10% 优化
  4. 资源极度受限时:必须找到最本质、最经济的解决方案
  5. 存在重大分歧时:团队争论不休,需要回到基础事实达成共识

不适合的场景:

  1. 有成熟解决方案的常规问题:比如做个登录功能,不需要重新发明轮子
  2. 时间紧急的情况:先用已知方案解决燃眉之急,后续再优化
  3. 成本敏感的场景:从零开始的成本可能远高于使用现成方案

第一性原理与工程实践

有人可能会问:强调第一性原理,是不是意味着否定所有的最佳实践和设计模式?

并不是。

第一性原理是一种思维方式,不是要你抛弃所有经验。正确的做法是:

  1. 理解原理:知道最佳实践背后的原理是什么
  2. 判断适用性:这个原理在当前场景下是否依然成立
  3. 灵活应用:根据实际情况调整,而不是生搬硬套

举个例子,「数据库索引可以提升查询性能」是个最佳实践。但背后的原理是什么?索引通过空间换时间,用额外的存储来加速查找。

那么在什么情况下这个实践可能不适用?

  • 表很小,全表扫描比使用索引更快
  • 写入频繁,索引维护成本太高
  • 查询模式复杂,单个索引无法覆盖

理解了原理,你就能做出正确的判断。

一个思维框架

最后,分享一个我常用的思维框架,帮助在实际工作中应用第一性原理:

第一步:定义问题

  • 问题的表象是什么?
  • 影响范围有多大?
  • 什么时候开始的?

第二步:收集事实

  • 哪些是可以测量的数据?
  • 哪些是已经验证的事实?
  • 哪些是推测和假设?

第三步:分解结构

  • 系统由哪些部分组成?
  • 各部分如何相互作用?
  • 问题可能出在哪个环节?

第四步:追溯原理

  • 这个环节的基本原理是什么?
  • 在当前场景下原理是否成立?
  • 有什么隐含的假设?

第五步:重构方案

  • 基于原理,最简单的解决方案是什么?
  • 这个方案的风险和成本如何?
  • 如何验证方案的有效性?

第六步:迭代优化

  • 实施后效果如何?
  • 是否还有改进空间?
  • 学到了什么新的认知?

写在最后

技术圈有句话:「没有银弹」。意思是没有一种技术或方法能解决所有问题。第一性原理也不是银弹,但它是一种强大的思维工具。

在这个技术快速迭代、框架层出不穷的时代,掌握第一性原理思维尤其重要。它能帮你在纷繁复杂的技术选择中保持清醒,找到问题的本质,做出正确的决策。

下次遇到棘手的技术问题时,不妨停下来问问自己:

「如果我是第一个遇到这个问题的人,没有任何前人经验可以借鉴,我会怎么思考?」

答案可能会让你惊喜。

真正的高手不是掌握了多少技术,而是掌握了技术背后的原理。当你能够从第一性原理出发思考问题时,你就真正理解了技术的本质。

这条路可能不太好走,需要不断质疑、思考、验证。但相信我,这是成为技术专家的必经之路。

与其做一个熟练的技术工人,不如做一个会思考的问题解决者。

以上。

从架构师的角度来看 AI 编程带来的技术债务

在吹水群,聊到 AI 编程,OZ 大佬提到

感觉 AI 会写很多各种可能情况的无用代码?它不会调试,不知道外部模块调用返回的具体格式,就用一堆if else去处理,最后还没有处理对。

GZ 大佬也说到:

ai 写代码感觉太差了,不知道在写什么
会不会制造更难维护的屎山

大家现在都已经在 AI 编程的世界中畅游了,整个软件开发似乎正以一种前所未有的方式悄然发生。 Cursor、Windsutf、Trae、lovable 等等已经完全进入了当前软件开发的世界中。

AI 能够根据自然语言注释或上下文,瞬间生成代码片段、函数甚至整个模块,极大地提升了编码的「表面」效率。我们正迈入一个「人机结对编程」的新时代。

但是大佬们也开始担心其产生的一些可能产生的后遗症。

今天我们从架构师的角度来看 AI 编程可能会给我们带来哪些技术债务。

作为架构师,我们的职责是超越眼前的速度与激情,洞察长期影响和潜在风险。

当团队的开发者们沉浸在「Tab」键带来的快感中时,一种新型的技术债务正在无声无息地累积。这种债务不再仅仅是糟糕的设计或潦草的实现,它更加隐蔽、更具迷惑性,并且直接与我们赖以信任的开发范式相冲突。

1992 年 Ward Cunningham 首次提出了传统的技术债务,指的是为了短期速度而采取了非最优的、有瑕疵的技术方案,从而在未来需要付出额外成本(利息)去修复。

它通常体现在糟糕的代码、过时的架构或缺失的文档上。

然而,AI 技术债务的范畴远超于此。它深深根植于数据、模型、基础设施乃至组织文化之中,其「利息」不仅是未来的重构成本,更可能是业务决策的失误、用户信任的丧失、合规风险的爆发,甚至是整个 AI 战略的崩塌。

从大模型的本质上来看,「 AI 编程是一个基于海量代码数据训练的、概率性的“代码建议引擎”」。它的工作方式决定了其产生的技术债务具有全新的特点。我们可以将其归纳为三个相互关联的维度:微观的代码级债务、宏观的架构级债务,以及深层的组织级债务。

微观的代码级债务 ——「似是而非」的陷阱

这是最直接、也最容易被感知的债务层面,它潜藏在 AI 生成的每一行代码之中。

在传统编程逻辑下,代码是要写的,而在 AI 编程时,代码是生成的,程序员的作用不再是写代码,而更多的是读代码,审核代码。

AI 生成的代码通常语法正确,甚至能够通过单元测试,但其内部逻辑可能存在微妙的、难以察觉的缺陷。例如,它可能选择了一个在特定边界条件下有问题的算法,或者「幻觉」出一个不存在的 API 调用,甚至在一个复杂的业务流程中,遗漏了一个关键的状态检查。这些 Bug 不再是明显的语法错误,而是「看似正确」的逻辑陷阱。

AI 编程减少了写代码的需要的认知成本,但是极大提升了读代码的心智负担。我们不仅仅要检查代码是否符合规范,还需要检查是否满足需求,以及是否在业务逻辑上完备。如果我们没有管这些问题,将来就可能是一个定时炸弹,隐藏在线上,不知道哪天会爆。

我们知道,AI 的知识来源于其训练数据——通常是海量的开源代码。因为 AI 倾向于生成「最常见」或「最流行」的解决方案,而不是针对当前上下文「最合适」的方案。它可能会引入一个庞大的库来解决一个小问题,或者使用一个过时但常见的编程范式,而不是团队正在推广的、更现代的模式。

这是 「设计熵增」的债务。它会持续不断地将外部的、非标准的、可能是平庸的设计模式注入我们的系统。长此以往,系统的技术选型会变得混乱,代码风格会变得不一致,精心设计的架构原则(如轻量级、高内聚)会被一点点侵蚀。我们必须警惕这种「随波逐流」的设计倾向。

每一行 AI 生成的代码,都应被视为一个来源不明的「外部依赖」。因为 AI 生成的代码片段可能悄无声息地引入了新的第三方依赖。更危险的是,它可能复现了其训练数据中包含的、有安全漏洞的代码模式(例如,不正确的加密实现、SQL 注入漏洞等)。此外,它生成的代码可能源自某种具有严格传染性的开源许可证(如 GPL),而团队并未意识到,从而引发法律合规风险。

为此,我们需要建立机制,自动扫描这些代码中的安全漏洞(SAST)和许可证合规问题。我们需要推动一种「零信任」的代码审查文化:开发者对任何由 AI 生成并最终提交的代码,负有 100% 的理解和责任。

宏观的架构级债务 ——「无声」的侵蚀

如果说代码级债务是「树木」的问题,那么架构级债务则是「森林」的水土流失。这种债务更加隐蔽,破坏性也更大。

如过往我们已经有一套优雅的微服务架构,定义了清晰的通信协议(如 gRPC)、统一的错误处理机制和标准的日志格式。然而,在使用 AI 编程时,为了快速实现一个功能,可能会接受一个使用 RESTful API、采用不同错误码、日志格式也千差万别的代码建议。单次来看,这只是一个局部的不一致,但当团队中数十个开发者每天都在这样做时,整个架构的一致性就会被破坏掉。

这是由于 AI 编程缺乏对我们「项目级」或「组织级」架构约定的认知而导致的,AI 编程是一个无状态的建议者,不理解我们系统的顶层设计。偿还这笔债务的成本极高,可能需要大规模的重构。架构师的核心挑战,从「设计」架构,扩展到了如何「捍卫」架构,防止其在日常开发中被无声地侵蚀。

AI 编程非常擅长生成「胶水代码」——那些用于连接不同系统、转换数据格式的脚本。这使得开发者可以轻易地在两个本应解耦的模块或服务之间建立直接的、临时的连接,绕过了设计的网关或事件总线。系统的模块化边界因此变得模糊,耦合度在不知不觉中急剧升高。

这是一种「捷径」。AI 让走「捷径」的成本变得极低,从而放大了人性中寻求最省力路径的倾向。架构师需要提供同样便捷、但符合架构原则的「正道」。例如,提供设计良好、文档清晰的 SDK、脚手架和标准化的 API 客户端,让「走正道」比「走捷径」更轻松。

从领域知识的角度来看,AI 可以从文档和代码中了解到一些,但是可能做不到完整的理解。

软件的核心价值在于其对复杂业务领域的精确建模。我们需要给 AI 以某种方式注入领域知识,如通过维护高质量的、富含领域术语的内部代码库和文档,来「引导」或「微调」AI 模型,使其建议更具上下文感知能力。

深层的组织级债务 ——「温水煮青蛙」的危机

这是最深层、也最关乎未来的债务,它影响的是人与团队。

当我们严重依赖 AI 编程时,会慢慢失去思考力和对代码的掌控力。

如果初级开发者过度依赖 AI,习惯于「提问-接受」的工作模式,而跳过了学习、思考和调试的艰苦过程。他们能够快速「产出」代码,但对底层原理、算法选择、设计权衡的理解却越来越肤浅。知其然不知其所以然,长此以往,团队成员的平均技能水平可能会停滞甚至下降。

这是团队的「未来」债务。我们在用未来的能力,来换取今天的速度。一个团队如果失去了独立解决复杂问题的能力,其创造力和韧性将不复存在。架构师需要倡导一种新的学习文化,将 AI 视为一个「助教」或「陪练」,而不是「枪手」。例如,鼓励开发者不仅要采纳 AI 的建议,更要尝试用自己的方法实现一遍,或者让 AI 解释它为什么这么写,并对解释进行批判性思考。

我们过往会用代码行数或者功能交付速度等指标来衡量团队的生产力,当有 AI 编程后,这些传统指标会得到巨大的提升,一天生产几千行代码是常事了。管理者可能会为此感到满意,但实际上,系统内部的技术债务正在快速累积,维护成本和风险也在同步攀升。

这是「技术管理」债务。我们需要建立新的、更能反映真实工程质量的度量体系。例如,关注代码的「可变性」(修改一个功能需要触碰多少文件)、「圈复杂度」、单元测试覆盖率的「质量」(而不仅仅是数量),以及 Code Review 中发现的深度问题的数量。架构师需要向管理层清晰地阐释 AI 编程的「债务风险」,推动建立更成熟的工程效能度量。

AI 编程助手是这个时代给予软件工程师最强大的杠杆之一,它有潜力将我们从繁琐的样板代码中解放出来,去专注于更具创造性的设计和思考。然而,任何强大的工具都伴随着巨大的责任和风险。

作为架构师,我们不能成为新技术的「勒德分子」,也不能成为盲目的「技术乐观派」。我们的角色,是确保这个强大的杠杆,成为放大我们架构意图和工程卓越的「放大器」,而不是制造技术债务的「复印机」。

这要求我们重新思考架构师的职责:我们不仅是蓝图的绘制者,更是蓝图的守护者;我们不仅要设计优雅的系统,更要设计能让优雅得以延续的「开发体系」;我们不仅要关注技术,更要塑造文化。通过建立清晰的规则、打造坚实的工程护栏、培育健康的开发者文化,我们才能确保,在 AI 赋能的未来,我们构建的软件系统,不仅跑得更快,而且走得更远、更稳。

以上。

架构师必备:MFA 了解一下

1. 引言

还记得 2023 年 GitHub 强制推行多因子认证(MFA)的那一刻吗?从 3 月开始,GitHub 分阶段要求用户启用 MFA,并在年底前全面完成覆盖,这让全球开发者不得不重新审视身份安全的重要性。

现在我们登录 Github ,除了要输入密码,还需要完成一个额外的验证步骤,比如输入手机上的动态验证码,或者通过手机上的身份验证器(Authenticator App)确认登录。这种看似繁琐的体验已经成为各大云厂商产品的标配。不仅是 GitHub,像 AWS、阿里云、腾讯云等云厂商也几乎都要求在敏感操作时使用多因子认证(MFA),以确保账户安全。

这种举措不仅保护了平台上的代码和账户安全,更体现了现代身份管理技术的趋势,今天,我们就从 GitHub 强制 MFA 的案例切入,了解 MFA 及 Google Authenticator 的实现原理。

2. 什么是 MFA/2FA

在探讨 MFA 之前,我们需要理解身份验证的本质。身份验证是确认某人或某物的身份是否属实的过程。无论是通过密码登录 Gmail,还是刷身份证进入火车站,身份验证的核心都是确保「你是你自称的那个人」。

然而,传统的基于密码的身份验证模式存在诸多隐患:

  • 密码过于简单:许多人使用诸如“123456”或“password”这样的弱密码。
  • 密码重复使用:用户往往将同一个密码应用于多个网站,一旦一个账户泄露,其它账户也岌岌可危。
  • 钓鱼攻击和暴力破解:黑客通过欺骗或技术手段轻易获取用户密码。
  • 中间人攻击:在不安全的网络环境中,密码可能被拦截。

这些问题导致密码的安全性备受质疑,因此需要额外的保护层,MFA 由此应运而生。

2.1 MFA:不是多一个步骤,而是多一层防护

MFA,Multi-Factor Authentication,多因子认证,是一种身份验证方法,要求用户提供多个独立的身份验证因素来完成登录或访问。传统的身份认证只依赖单一密码,MFA 则通过引入额外的验证步骤,极大地提升了账户安全性。

在 MFA 中,通常会结合以下三类验证因素:

  • 你知道的东西:密码、PIN 码、答案问题等。
  • 你拥有的东西:动态验证码(通过手机或硬件设备生成)、安全令牌、智能卡、U 盾等。
  • 你自身的特征:生物特征验证,如指纹、面部识别、虹膜扫描等。

MFA 的意义在于,即便攻击者获得了你的密码,由于缺少额外的验证因素,他们依然无法轻易访问你的账户。例如,登录 GitHub 时,即使密码被泄露,攻击者若没有你的手机或安全密钥,仍然无法完成登录。

毕竟,密码泄露已经成为网络攻击中最常见的手段,而 MFA 则为用户的账户增加了第二道甚至第三道锁。

2.2 2FA

2FA 是MFA 的一种特殊形式,它仅使用两种不同的验证因素来完成认证。简单来说,2FA 是 MFA 的一个子集。

例如:

  • 登录时输入密码(第一个验证因素:你知道的东西)。
  • 然后输入手机上的动态验证码(第二个验证因素:你拥有的东西)。

值得注意的是,两种不同的验证因素是类别的不同,像以前有一种策略是需要提供密码和安全问题答案,这是单因素身份验证,因为这两者都与「你知道的东西」这个因素有关。

在大多数应用中,2FA 已经足够满足安全需求,因此它是目前最常见的多因子认证实现方式。

3. 为什么 MFA 如此重要?

1. 密码不再安全

随着技术的进步,密码破解的门槛越来越低。攻击者可以通过以下方式轻松破解密码:

  • 暴力破解:通过快速尝试各种可能的密码组合。
  • 数据泄露:黑客通过暗网购买被泄露的用户名和密码。
  • 钓鱼攻击:通过伪装成合法网站诱骗用户输入密码。

在这种背景下,仅靠密码保护账户变得极为不可靠。MFA 通过引入多层保护,从根本上提升了安全性。

2. 提高攻击成本

MFA 的最大优势在于,它大幅提高了攻击者的攻击成本。例如,攻击者即便成功窃取了用户密码,也需要物理接触用户的手机或破解生物特征才能完成登录。这种额外的复杂性往往会使攻击者放弃目标。

3. 应对多样化的威胁

MFA 可以有效抵御多种网络威胁,包括:

  • 凭证填充攻击:即使用泄露的密码尝试登录多个账户。
  • 中间人攻击:即便密码在传输中被窃取,攻击者仍需第二个验证因素。
  • 恶意软件:即使恶意软件记录了用户输入的密码,也无法破解动态验证码。

4. MFA/2FA 的工作过程和形式

4.1 MFA 验证的形式

MFA 形式多样,主要有如下的一些形式:

  1. 基于短信的验证:用户在输入密码后,会收到一条包含验证码的短信。虽然方便,但短信验证并非绝对安全,因为短信可能被拦截或通过 SIM 卡交换攻击(SIM Swapping)被窃取。
  2. 基于 TOTP(时间同步一次性密码)的验证:像 Google Authenticator 这样的应用程序可以生成基于时间的动态密码。这种方式更安全,因为动态密码仅在短时间内有效,且无需网络传输。
  3. 硬件令牌:硬件令牌是专门生成动态密码的物理设备。例如银行常用的 USB 令牌,用户需要插入电脑才能完成验证。
  4. 生物特征验证:指纹、面部识别和视网膜扫描是最常见的生物特征验证方式。这种验证方式非常直观,但存在用户数据隐私的争议。
  5. 基于位置的验证:通过 GPS 或 IP 地址限制用户只能在特定位置登录。
  6. 基于行为的验证:通过分析用户的打字节奏、鼠标移动轨迹等行为特征来确认身份。

4.2 2FA 如何工作?

双因素身份验证的核心理念是:即使攻击者获得了用户的密码,他仍然需要通过第二道验证关卡才能访问账户。以下是 2FA 的典型工作流程:

  1. 第一道验证:用户输入用户名和密码:用户通过密码证明「知道的内容」,这是第一道验证因素。
  2. 第二道验证:动态代码或生物特征识别:系统会向用户发送一个一次性验证码(如短信、电子邮件或 Google Authenticator 生成的代码),或者要求用户提供指纹或面部识别。这是「拥有的东西」或「自身的特征」的验证。
  3. 验证成功,授予访问:如果两道验证都通过,用户即可成功登录。

如当你登录阿里云时,输入密码后需要打开阿里云的 APP,输入 MFA 的验证码。

5. MFA 的局限性

尽管 MFA 极大地提高了账户安全性,但它并非万能。有如下的一些局限性:

  1. 用户体验问题:对于技术不熟练的用户来说,设置和使用 MFA 应用程序门槛比较高。此外,每次登录需要额外的验证步骤,也可能降低用户体验。

  2. 成本问题:企业需要支付额外的费用来实施 MFA。例如短信验证需要支付短信发送费用,而硬件令牌的采购和分发也需要额外开支。

  3. 并非百分百安全:MFA 虽然有效,但并非无懈可击。例如:

    • 短信验证可能被攻击者通过 SIM 卡交换攻击破解。
    • 恶意软件可能会窃取动态密码。
    • 高级攻击者甚至可能通过社会工程学手段获取验证码。

在了解了概念后,我们看一下我们常用的一个 MFA 验证应用 Google Authenticator 的实现原理。

6. Google Authenticator 的实现原理

在使用 Google Authenticator 进行 2FA 的过程中,验证的过程可以分为以下两个主要阶段:初始化阶段 和验证阶段

6.1 初始化阶段:共享密钥生成与分发

这是用户首次启用双因素身份验证时发生的过程。在此阶段,服务端生成共享密钥(Secret Key)并通过安全的方式分发给用户的 Google Authenticator 应用。

  1. 服务端生成共享密钥

    • 服务端为用户生成一个随机的共享密钥K(通常是 16~32 个字符的 Base32 编码字符串,例如JBSWY3DPEHPK3PXP)。
    • 该密钥会作为后续动态密码生成的核心,必须对外保密。
  2. 生成二维码

    • Example: 服务提供方的名称。
    • username@example.com: 用户的账户。在 github 的场景中这个字段是没有的。
    • SECRET=JBSWY3DPEHPK3PXP: 共享密钥。
    • issuer=Example: 服务提供方名称(用于显示在 Google Authenticator 中)。
    • 服务端将共享密钥和其他元信息(如站点名称、用户账户)打包成一个 URL,符合otpauth:// 协议格式,例如:

      otpauth://totp/Example:username@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
      

      其中:

    • 该 URL 会被编码为一个二维码,供用户扫描。

  3. 用户扫描二维码

    • 用户使用 Google Authenticator 应用扫描二维码,应用会解析出共享密钥(K)以及站点相关信息,并将其安全存储在手机本地。
    • 共享密钥在手机端不会传回服务端,所有计算均在本地完成。
  4. 初始化完成

    • 用户的 Google Authenticator 应用现在可以基于共享密钥K 和当前时间生成动态密码。
    • 服务端同时将该共享密钥K 绑定到用户账户,并妥善保存以便后续验证使用。

6.2 验证阶段:动态密码的生成与验证

这是用户登录时的验证过程。在此阶段,客户端和服务端基于相同的共享密钥K 和时间步长计算动态密码,并进行验证。

6.2.1 客户端生成动态密码

  1. 获取当前时间

    • Google Authenticator 应用从设备的系统时间中获取当前的 Unix 时间戳(以秒为单位)。
  2. 将时间戳转换为时间步长

    • 将时间戳除以时间步长(通常为 30 秒),并取整:

      T = floor(currentUnixTime / timeStep)
      

      例如,当前时间是1697031000 秒,时间步长为 30 秒,则:

      T = floor(1697031000 / 30) = 56567700
      
  3. 计算 HMAC-SHA-1 哈希值

    • Google Authenticator 将时间步长T 转换为 8 字节的 Big-endian 格式(例如0x00000000056567700)。
    • 使用共享密钥K 和时间步长T 作为输入,计算 HMAC-SHA-1 哈希值:

      HMAC = HMAC-SHA-1(K, T)
      

      结果是一个 20 字节(160 位)的哈希值。

  4. 截断哈希值

    • 根据 HMAC 的最后一个字节的低 4 位,确定一个偏移量offset
    • 从 HMAC 中偏移量开始,提取连续 4 个字节,生成动态二进制码(Dynamic Binary Code,DBC)。
    • 对提取的 4 字节数据按无符号整数格式解释,并将最高位(符号位)置零,确保结果为正整数。
  5. 取模生成动态密码

    • 对动态二进制码取模10^6,生成 6 位数字密码:

      OTP = DBC % 10^6
      

      例如,计算结果为123456

  6. 显示动态密码

    • Google Authenticator 将生成的 6 位动态密码显示给用户,该密码有效时间为一个时间步长(通常为 30 秒)。

6.2.3 服务端验证动态密码

  1. 服务端获取当前时间

    • 服务端同样获取当前的 Unix 时间戳,并计算对应的时间步长T
  2. 计算候选动态密码

    • 服务端使用用户账户绑定的共享密钥K 和当前时间步长T,通过与客户端相同的 TOTP 算法计算动态密码。
    • 为了容忍客户端和服务端的时间差异,服务端通常会计算当前时间步长T 以及前后几个时间步长(例如T-1 和T+1)的动态密码,形成候选密码列表。
  3. 验证动态密码

    • 如果匹配成功,则验证通过,用户被允许登录。
    • 如果所有候选密码都不匹配,则验证失败,拒绝用户登录。
    • 服务端将用户提交的动态密码与候选密码列表逐一比对:

6.3 关键数据的传递过程

在整个验证过程中,关键数据的传递和使用如下:

6.3.1初始化阶段

  • 服务端 → 客户端
    • 共享密钥(K):通过二维码或手动输入传递给 Google Authenticator。
    • 站点信息:站点名称、账户名等信息也通过二维码传递。

6.3.2验证阶段

  • 客户端

    • 本地保存的共享密钥K 和当前时间计算动态密码。
    • 用户将动态密码(6 位数字)手动输入到登录页面。
  • 客户端 → 服务端

    • 用户提交动态密码(6 位数字)和其他常规登录凭据(如用户名、密码)。
  • 服务端

    • 使用同样的共享密钥K 和时间步长计算候选动态密码。
    • 对比用户提交的动态密码与计算结果,完成验证。

整个过程有如下的一些关键点:

  1. 共享密钥的安全性

    • 共享密钥K 是整个验证过程的核心,必须在初始化阶段通过安全的方式传递,并在客户端和服务端妥善保存。
    • 密钥不会在验证阶段传输,只有动态密码被提交。
  2. 时间同步

    • 客户端和服务端的时间必须保持同步,否则计算的时间步长T 会不一致,导致动态密码验证失败。
    • 为了适应设备的时间漂移,服务端通常允许一定的时间步长偏移(如 ±1 步长)。
  3. 动态密码的短生命周期

    • 动态密码的有效时间通常为一个时间步长(30 秒),即使密码被窃取,也很快失效。
  4. 离线生成

    • 动态密码的生成完全依赖共享密钥和时间,无需网络连接,增强了安全性。

7. 小结

通过 GitHub 强制推行 MFA 的案例,我们可以清晰地看到,MFA 已经成为现代身份管理的重要基石。密码本身的弱点让账户安全长期处于威胁之下,而 MFA 的引入不仅为用户增加了一层甚至多层防护,更在技术上为身份验证树立了一个全新的标准。

尽管 MFA 并非完美,还存在用户体验、实施成本和一定的攻击风险,但它在密码安全性危机中提供了一种强有力的解决方案。无论是个人用户还是企业,采用 MFA 已经成为抵御网络威胁的必要手段。

未来,随着技术的进一步发展,多因子认证可能会越来越多地融合生物特征、行为分析和人工智能技术,为用户提供更安全且更便捷的身份验证体验。而对于每一位开发者和用户来说,理解和使用这些技术,不仅是保护自身数字资产的关键,更是应对日益复杂的网络安全形势的必修课。

以上。