从安全说起

你听说过黄金法则吗?我是从极客时间里何为舟老师的《安全攻防技能 30 讲》课程中学到的这个法则,它指的是认证授权审计

安全攻防技能30讲_安全_漏洞_黑客-极客时间

1705297611055 d777045f d945 4e9a a18c 8c74044ff8c0

这和黄金有什么关系?这要从它们三个概念的英文单词说起,认证就是 Authentication,而授权对应的是 Authorization,审计呢,对应的英文是 Audit。细心的同学们应该已经发现了,它们三个单词的前两个字母都是“Au”,而在高中化学课上我们学到了,金元素对应的字母表示正是“Au”。这便是“黄金法则”的由来。

当然,除了叫做“黄金法则”外,有时也会只取这三个单词中的首字母,称为 AAA 法则,或者叫 3A 法则。如果 3A 法则你也没听过,那么,A & A 你一定经常听到过吧?这时,就是省去了审计,A&A 指的就是认证和授权(即 Authentication & Authorization,时常也会被缩写为 AN 和 AZ,或者 AuthN 和 AuthZ)。

是的,认证和授权尽管联系紧密,以至于很多人傻傻分不清,但其实是有区别的。它们的区别可以用下图表示:

1705291610833 261aa57d 735e 457b 8548 e5706678a495

当然,极客时间的课程里给我们讲得更加丰富,除了认证和授权,还加入了审计。它们的关系如下图:

1705291713938 11c9e078 268b 44bb b12d c87032a0ec97

总之,任何计算机系统要做到安全,都离不开“认证”、“授权”和“审计”这三个模块。

认证的对象和场景

认证的对象可以分为两类,这是我从极客时间里周志明老师的《周志明的软件架构课》中学到的,它们分别是“人类”和“机器”。它们的英文表示比较有意思,不是字面翻译,但是更加传神。分别是“Request Authentication”和“Peer Authentication”。

周志明的软件架构课_软件架构_分布式系统_基础设施_架构演进_单体架构_SOA架构_微服务_云原生-极客时间

1705297977354 c8a59e92 b963 443f 9fcc d1de807e763a

以上按认证对象,将认证分成了“人类”和“机器”两种类型。而我个人,将它加以扩展,将它细分成“ABCDE”5 类,可能 2B 和 2C 大家也都听过,比较熟悉。但是 2A、2B、2C、2D 和 2E 这种同花顺式的分法,我之前从来没有见过,现在我在这里将它提出来,说不定以后这种分类法流行了起来,就会成为我的一个小小独创呢

1705292590678 0945b1ca 2cff 4ef0 beeb ae93c55f6c37

这种同花顺式的分类法,也可以用在企业认证场景中。企业认证场景,也是可以粗分为两类,分别是“对内”和“对外”,而进一步也可以细分为“ABCDE”,如下图所示:

1705292769019 aef3aa78 b68f 4ddf a2f7 8a822e791bb2

2A,<font style=color:rgb(0, 0, 0);>面向 API 的身份认证

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>在许多应用程序中,API(应用程序接口)扮演着关键的角色,用于数据交换和服务访问。2A(To API)身份认证场景是指在 API 访问中使用身份认证来验证请求的合法性。这种场景下,身份认证通常使用 API 密钥、令牌或其他形式的凭证。

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>这种场景,其实也跨了对内和对外。一般建议有 2 个 API 网关,一个网关面向内部,一个网关面向外部。但是网关的作用都是类似的,都会对 API 的调用者鉴权。一个企业内部会存在多个领域和子领域,能力的复用也可以通过 API 的形式来提供。

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>API 可以分为同步 API 和异步 API,同步 API 多是我们熟悉的 Restful API、或者是 GraphQL。而异步 API 一般应用了事件驱动架构。建议的方式是领域内部通过同步 API 来调用,而跨领域之间使用异步 API 以解耦。

2B,<font style=color:rgb(0, 0, 0);>面向企业合作伙伴的身份认证

<font style=color:rgb(0, 0, 0);>企业合作伙伴之间的信息交换和资源共享需要进行身份认证以确保安全性和可信度。2B(To Business)身份认证场景是指企业间的身份认证,允许合作伙伴之间进行受控的访问和交互。

<font style=color:rgb(0, 0, 0);>2C,面向客户的身份认证

<font style=color:rgb(0, 0, 0);>面向客户的身份认证是指为终端用户提供访问应用程序或在线服务的身份验证机制。2C(To Consumer 或者说是 To Customer)身份认证场景在电子商务、社交媒体和在线银行等领域非常常见。这种场景下,常用的认证方式包括用户名密码登录、社交登录和多因素身份验证等。

<font style=color:rgb(0, 0, 0);>关于该领域的详细探讨,可以参考 Simon Moffatt 编写的《Consumer Identity & Access Management Design Fundamentals<font style=color:rgb(0, 0, 0);> 》一书。

<font style=color:rgb(0, 0, 0);>当然,这是从技术角度探讨。从商业角度,我最近也学习了一个观点:要将焦点从客户转移到用户(《From customer to user》)。

1705294512711 5e7646d7 92af 4ec1 9c6c afbf57a62849

<font style=color:rgb(0, 0, 0);>这是什么意思呢?原来作者主张,客户是付费购买你的产品的,而用户的范围更广,他们可能还没有成为你的客户,但是却非常重要,原因是对于潜在客户来说,这些用户的声音更有影响力。比如你在看一个商品时,如果是商家自己说好、或者已经购买的人说好,可能都不太会相信。对于前者,有王婆卖瓜的嫌疑;对于后者,更像是托(比如尊敬的某来车主?)。但如果一个路人说好,你可能会更加采信。该书作者举了一个生动的案例:很多人喜欢在宜家的沙发上坐着,甚至有人在床上躺着睡觉。本来宜家的老板是想赶这些人走的,但后来却被商业顾问建议,不仅不要赶走他们,甚至应该邀请更多人来商场葛优躺。原因是这些占了便宜的“用户”们在社交媒体或者亲朋好友之间随意闲聊时,说起这些体验,就会在其朋友圈产生出比起商业广告之类的宣传大得多的影响力。

1705294529797 6c647545 2b45 4a3e adfe d0262172e923

<font style=color:rgb(0, 0, 0);>受此观点启发,我有点想将 2C 定义为“面向用户的身份认证”,但无奈没有找到合适的英语单词能够以 C 开头代表用户,于是仍然使用了面向“客户”,但我这里指的客户,并不只是付费用户,而是所有的用户。

<font style=color:rgb(0, 0, 0);>2D,面向开发者的身份认证

<font style=color:rgb(0, 0, 0);>2D(To Developer)身份认证场景是指开发者之间进行身份认证,以便在开发过程中访问受保护的资源和服务。这种场景下,通常使用开发者密钥、API 密钥或令牌进行认证。

<font style=color:rgb(0, 0, 0);>在企业内部,有很多供开发者使用的系统,常见的日志系统、监控系统等等。这种情况下,开发者是企业的内部员工的一个子集。当然,还有面向外部开发者的系统,比如大的平台,都会提供开发者门户网站、或者开放平台等,这时面向的主要就是外部开发者了。

<font style=color:rgb(0, 0, 0);>谈到一门生意,一般我们会想到 2B 或者 2C,而实际上,2D 也可以做成很大的生意,比如苹果,比如微信,其 2D 的业务如下图所示。

1705294586973 f379f200 1e03 47fe bf0c 2f75d910ee69

<font style=color:rgb(0, 0, 0);>这时 2D 的身份认证建设就至关重要。

<font style=color:rgb(0, 0, 0);>2E,面向内部员工的身份认证

<font style=color:rgb(0, 0, 0);>企业内部系统和资源的访问需要对内部员工进行身份认证。2E(To Employee)身份认证场景是指面向内部员工的身份验证,确保他们只能访问其所需的受保护资源和权限。

<font style=color:rgb(0, 0, 0);>解决方案

前面提到,要实现一个安全的 IT 系统,“认证”、“授权”必不可少。但每次都要从头开始实现,不仅成本巨高而不可行,甚至还是不安全的。因为安全是一个非常专业的领域,自己定制化的方案往往会有各种安全隐患。

好在,市面上有非常多的解决方案可供选择,比如 Okta、Auth0、AWS cognito、Entra ID、Authing.cn 身份云等等云解决方案数不胜数。而开源方案也有很多,比如我比较熟悉的 Keycloak、Duende IdentityServer 等等。

对接方式

专业的事情交给专业的人或者团队去做,我们只需要做调包侠就行了。但是调包,也有不同的情况。以下分类讨论一下。

购买的成熟产品

有时候,自行开发一套产品,不如直接购买来得快速与经济。对于成熟的产品,要对接现有的认证授权解决方案,一般以配置为主,没有太多发挥空间。即这个产品提供了哪种配置方式,就按照它的方式来进行配置就好。以前写过一些案例,比如这篇《在自托管 GitLab 实例中集成 Keycloak 登录 - Jeff Tian的文章 - 知乎 》就详解了在自托管的 GitLab 中对接 Keycloak 的步骤。

1705295740511 3448492c e2d3 4b3a bc41 f63eb847504a

自建服务

由于微服务架构以及前后端分离的开发方式的流行,我这里也按从前到后的情况分类列举一些案例分享。

前后端分离开发尽管有很多优势,但也带来了不少挑战,《前后端分离开发中前端需要克服的挑战 - Jeff Tian的文章 - 知乎 》列举了几个常见的挑战以及解决方案。

纯前端应用

这是指没有服务器端支持的 SPA,由于前端框架众多,所以这里再分类列举一下。

UMI 3

实战案例:在 Umi Js 项目中通过 umi-plugin-oauth2 插件对接 Keycloak - Jeff Tian的文章 - 知乎 》 有一个详细的例子。其中使用了 umi-plugin-oauth2,它是我从别人那里 fork 过来之后,进行了一些优化之后发布的 npm 包,但是只适用于 UMI 3,对于 UMI 4,有一些问题,详见代码库的 Issues,希望有好心人帮忙解决一下!

1705295693479 4d37cf26 8731 4526 a253 64d3aaa468fb

VUE

使用 IdentityServer 保护 Vue 前端 - Jeff Tian的文章 - 知乎 》举了一个详细的例子,而《使用 vuex-oidc 在 Vue 项目中对接 Duende IdentityServer - Jeff Tian的文章 - 知乎 》又再次使用 vuex-oidc 在 VUE 应用中的使用给出了详细的对接步骤。

1705295841946 1e8f613a 37af 45b4 9eb5 c1aad98a14d4

NextJs

用技术直觉力解决棘手问题 - Jeff Tian的文章 - 知乎 》 以一个问题排障为引子,详细介绍了如何在 NestJs 里利用 Next-Auth 来对接身份认证服务。

1705296008128 87ff30dc 52ad 4c45 bbb7 58db43986ca5

BFF 层

尽管纯前端对接身份认证服务是完全可行的,但是属于“不得已”而为之。如果有服务器端,还是应该在服务器端对接身份认证服务。如果在前端进行对接,就要使用公开客户端,因为将密钥保存在前端,非常容易泄露,从而使用保密客户端没有意义。在不得已要在纯前端中对接身份认证时,就一定要对跳转链接进行验证,并且一定要开启 PKCE。详见《抛弃隐式许可,拥抱 PKCE - Jeff Tian的文章 - 知乎 》中的论述。

1705296289848 10df8bd6 91b5 46ce b38b af31db63cf35

关于 BFF,我也有一些个人观点,甚至杜撰了一些名词,详见《BFF 进化 - Jeff Tian的文章 - 知乎 》中的陈述。

如果是使用 Java Spring Boot 写 BFF 层,则可以使用 spring-security-oauth2-client,基本上只需要配置一下。

yaml spring: security: oauth2: client: registration: idp: clientId: xxx clientSecret: yyy redirectUri: zzz authorizationGrantType: authorization_code scope: [openid, email, offline_access] provider: idp: authorizationUri: xxx/connect/authorize tokenUri: xxx/connect/token jwkSetUri: xxx/.well-known/openid-configuration/jwks issuerUri: xxx userNameAttribute: email

1705296431914 1aceba69 048b 4919 b92e 32829fc990ee

后端领域服务层

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>在前面我们介绍了前端代理服务器 BFF(Backend For Frontend)的概念和作用。它是一个位于前端应用与后端领域服务之间的中间层,负责聚合和转换数据,提供定制化的接口给前端应用使用。

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>而领域服务是运行在服务器上的后端服务,它们负责处理特定领域的业务逻辑和数据持久化。领域服务通常被组织为一组微服务或模块,每个服务或模块负责处理特定的领域功能。

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>尽管领域服务和 BFF 都运行在服务器上,但它们在功能和定位上有一些区别:

  • <font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>领域服务的职责<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>:领域服务是负责处理具体领域业务的服务,它们关注业务逻辑、数据持久化和领域模型的实现。领域服务通常是面向内部系统和后端业务流程的。
  • <font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>BFF 的职责<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>:BFF 是前端应用与后端领域服务之间的中间层,它负责为前端应用提供定制化的接口和数据聚合。BFF 的目标是提供更好的用户体验和前端开发效率,它关注的是前端应用的需求和数据交互。
  • <font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>数据处理的角度<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>:领域服务更关注业务逻辑和数据持久化,它们会对数据进行处理、验证和持久化。而 BFF 则更关注数据的聚合和转换,将多个后端服务的数据整合成前端需要的形式。

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>总的来说,领域服务和 BFF 在职责和关注点上有所不同。领域服务负责处理具体的领域业务逻辑,而 BFF 则负责为前端应用提供定制化的接口和数据聚合,以满足前端的需求。

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>在后端领域服务层对接身份认证,与纯前端和 BFF 层有着非常大的不同!纯前端也好,BFF 层也好,在对接身份认证时,主要偏向于用户交互,通过用户的授权,实现对用户令牌的<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>获取<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>。而在后端领域服务层中对接身份认证,主要目的是直接或者间接地通过身份认证服务来<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>验证<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>令牌。

<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>这个不同,还体现在它们在身份认证流程中的角色不同。首先,前端或者BFF,都充当了第三方应用的角色;而后端领域服务,往往是资源服务器的角色。

1705297074251 fcad9d2b ec75 441e 8994 cc6a02bfaa25

如果使用 Java Spring-Boot 做后端领域服务的开发,可以使用 spring-security oauth2 resource server (<font style=color:rgb(25, 27, 31);>spring-boot-starter-oauth2-resource-server)来对接身份认证服务。

yaml security: oauth2: resource: url: xxx jwk: key-set-uri: https://yyy/.well-known/openid-configuration/jwks token-type: Bearer token-info-uri: https://yyy/connect/token

1705297139536 91b78e0f bdb3 479d ab11 48db47687f4e

当然,不使用**<font style=color:rgb(25, 27, 31);>spring-boot-starter-oauth2-resource-server**<font style=color:rgb(25, 27, 31);>也是可以的,只是代码量会很大,详见《[不使用 spring-boot-starter-oauth2-resource-server,如何使用 OIDC Server 保护 API? - Jeff Tian的文章 - 知乎](<font style=color:rgb(25, 27, 31);>https://zhuanlan.zhihu.com/p/644394539<font style=color:rgb(25, 27, 31);>) 》中的讨论。

<font style=color:rgb(25, 27, 31);>能够使用<font style=color:rgb(25, 27, 31);>spring-boot-starter-oauth2-resource-server<font style=color:rgb(25, 27, 31);>尽量使用,但有时,需要一些特殊的用法而又难以对它进行扩展时,就不得不自己写代码了。这些特殊用法,这里《[通过 Bean 的方式扩展 Spring 应用,使其同时支持多个授权服务颁发的令牌。 - Jeff Tian的文章 - 知乎](<font style=color:rgb(25, 27, 31);>https://zhuanlan.zhihu.com/p/626130631<font style=color:rgb(25, 27, 31);>) 》有一个实际例子。

<font style=color:rgb(25, 27, 31);>

<font style=color:rgb(25, 27, 31);>总结

用一张脑图总结今天的内容吧:

1705311015473 aae1230f d630 4437 b52b 3809c3e145c5