计算机领域的任何问题都可以通过增加一个间接的中间层来解决……除非是中间层太多了。

什么是一团乱麻问题?

两个层之间分别有多个应用或者服务,而它们之间存在多对多的调用关系,如下图所示:

image.png

这样,如果有n个应用,m个服务,相互调用的关系就是 nxm 的规模。而且,每当增加一个应用,都需要 m 规模的开发量去分别对接上层的服务。而每增加一个服务,也需要改动 n 规模的应用。

解决方案

通过引入一个代理层,将上层所有应用的请求收拢至一个点,然后代理所有请求至下层的服务。这样在层数上增加了一层,并将一个多对多的映射关系,变换成了分别是一个多对一的关系和另一个一对多的关系,如下图所示:

image.png

这样有什么好处?

这样,通过引入了代理层,就将上层应用和下层服务解藕了。它们之间不再直接沟通,而是通过代理层中转。这样的好处是,每当增加一个应用,该应用只需要和代理层对接一次,就能享受到所有 m 个服务;而同样,当增加一个服务时,应用层完全无感知,不用做任何修改。

所以,引入这个代理层带了一点点复杂性,但是同时却节省了大量的开发量!这个解决方案,不仅仅是让架构图看起来更清晰和有条理,更能让实际的开发效率翻不知道多少倍。

案例1:身份认证代理

通过引入身份认证代理实现的身份认证功能开发上的效率碾压,实现了软件开发中的身份认证自由。显然,几乎所有的软件应用都需要身份认证功能,而为每个应用单独去实现这个功能,不仅乏味,且很难在短时间内对接众多的不同身份源。然而,通过引入身份认证代理,每个应用就只需要集成一次,便能立即对接到这个代理之后丰富的身份源,并且,每当有新的身份源时,只需要在这个代理中集成一次,便能赋能到每一个前端应用。

其实,我回顾了一下最近几年写的文章,大多都是在介绍一些具体的身份认证代理实现细节。

Authing

身份云 authing.cn 是一个 IDaaS,除了对接外,没有额外的开发量。我自己做了一些例子:

哈德韦的个人小程序

Brickverse

https://www.brickverse.net/login
A Dream: What is Brickverse? - Jeff Tian的文章 - 知乎

你会看到,这两个前端应用的登录界面很像,因为它们都是 Authing 提供的。虽然可以自定义界面,但如果你像我一样懒,那么默认主题就是 Authing 提供的了。你可以选择不同的登录方式,具体可以有多少种登录方式,取决于在 Authing 的后台配置多少了。需要强调的是,你可以在 Authing 后台配置很多身份源,然后,所有的前端应用(比如上面列举的两个),就能立刻使用上了。

image.png

当然,这里的前端应用并不局限于前端技术,对于身份认证来说,哪怕是后端服务,在这个语境里,也是前端应用。但从展示方便的角度来说,这里列举的只是前端技术层面的前端应用(但你要知道,列举的只是少数几个而已)。

Keycloak

Keycloak 是一个基于 Java 的开源身份认证解决方案,使用它和使用 Authing 一样,除了对接,几乎没有开发量。虽然 Authing 提供私有化部署,但如果你想我一样懒,可以直接使用其 SaaS 服务,省去部署。而 Keycloak 一般来说,是需要自己部署的,一个比较省钱的方案,是部署到 Heroku(参考《FreeArch: 一键拥有你自己的身份认证平台 Keycloak! - Jeff Tian的文章 - 知乎 》)。当然,也可以使用 SaaS 方案,省去自己部署的麻烦,详见:《https://www.amazonaws.cn/en/solutions/keycloak-on-aws/》。

egg js 对接示例

http://uniheart.pa-ca.me/keycloak/login
在 eggjs 中集成 Keycloak 用户认证 - Jeff Tian的文章 - 知乎
image.png

基于 UmiJs 的纯前端对接示例

https://umi-ckeditor5.brickverse.dev/
实战案例:在 Umi Js 项目中通过 umi-plugin-oauth2 插件对接 Keycloak - Jeff Tian的文章 - 知乎

基于 SpringBoot 的后端对接示例

https://tranquil-plains-58233.herokuapp.com/visitor
Free Arch: 如何在 Spring Boot 应用中集成 Keycloak? - Jeff Tian的文章 - 知乎

命令行应用对接示例

没错,除了前端、后端项目外,命令行工具也可以享受到身份认证代理带来的好处!这里有一个例子:《OAuth 2.0:对接 Keycloak 设备码授权流程 - Jeff Tian的文章 - 知乎

如果安装了 npm,使用如下两行即可体验: shell npm i -g k8ss k8ss login

可以看到,以上多个应用,其登录界面都相同,因为对接了同一个身份认证代理: https://keycloak.jiwai.win

IdentityServer

Duende IdentityServer 是基于 ASP.NET 的身份认证解决方案,相比 Keycloak,它没有内置完备的 UI:免费的示例 UI 提供的功能特别少,而要使用完整的管理后台的话,需要付费购买 AdminUI。它的核心功能,都可以免费使用,前提是你的代码得开源。如果商用的话需要购买许可证。

我之前写过如何部署 IdentityServer 的文章,详见这两篇:《部署到 Azure 网站应用 IdentityServer 初体验 - Jeff Tian的文章 - 知乎 》和《Free Arch: 将 IdentityServer 部署到 Okteto - Jeff Tian的文章 - 知乎 》。

部署到 Azure 后的应用实例有: https://id6.azurewebsites.net/Account/Login
image.png

基于 UmiJs 的纯前端对接

使用 IdentityServer 保护 Vue 前端 - Jeff Tian的文章 - 知乎

基于 AntD Pro 的前端对接

使用 IdentityServer 保护 Web 应用(AntD Pro 前端 + SpringBoot 后端) - Jeff Tian的文章 - 知乎

基于 SpringBoot 的后端对接

使用 IdentityServer 保护 Web 应用(AntD Pro 前端 + SpringBoot 后端) - Jeff Tian的文章 - 知乎

命令行应用对接示例

这个示例还是 k8ss,通过指定 idp 参数,可以切换指向身份认证代理到部署在 Azure 上的 identityserver: k8ss login --idp=id6:
OAuth 2.0:对接 IdentityServer 设备码授权流程 - Jeff Tian的文章 - 知乎

案例2:API 网关

如果没有网关,采用前端直接调用后端服务的模式,就会是这样的状态:
image.png

在这样的架构下,如果又增加了一个前端,比如说需要一个在汽车智慧屏上显示的应用,并且后端服务也增加了,这时调用关系就变成了一团乱麻。
image.png

可以通过引入一个网关层来解决这个问题,该网关层收拢所有前端的调用,再转发给后端,调用关系会简单清晰很多。虽然应用在了不同的场景,但是架构图画起来,和案例1中的身份认证代理一模一样。

AWS API Gateway

通过 AWS API Gateway,只需要在界面上可视化做些配置即可实现对后端服务的编排,不需要编码了。
image.png
我之前写了一个前端应用,后来需要文件上传的能力时,写了一个新的服务,只需要使用 AWS API 网关的界面配置上去,前端就能使用该服务了,不需要对接新的 API Endpoint,十分方便。不过,对于文件上传来说,在配置上有一个要注意的地方,详见:《AWS API Gateway 踩坑记 -- Binary Media Types - Jeff Tian的文章 - 知乎 》。

手写 API 网关

除了 AWS API Gateway,其实也可以手写网关。其实,我在 2018 年至 2020 年,做的事情总结下来就是在用 nodejs 手写 API 网关。这一点在《编排还是编舞,暨近几年工作回顾 - Jeff Tian的文章 - 知乎 》里提到了。

这种模式相信很多公司现在仍然在采用,大概有几个原因:一是团队的习惯。在一个团队中,引入改变并不容易,还得慢慢来。二是有复杂的逻辑,我之前在一个大型跨国企业,他们在全球的 APP 中使用了同一个网关,但是给到不同国家的用户体验完全不一样。所以通过手写网关,实现了一种策略模式,来使用同一个代码库,来对不同国家不同的底层实现提供支持。三是使用了一些特别的协议,比如下面这种:

案例3 :GraphQL 网关

GraphQL 实现网关特别容易,不需要 AWS API Gateway 就可以自己实现。但是要求后端服务都暴露 GraphQL 接口。这样,使用 Apollo Gateway 框架,就可以自己搭建一个 GraphQL 网关。

我自己搭建了一个简单的,详见《Free Arch: GraphQL Federation 初体验(成功,且丝滑!) - Jeff Tian的文章 - 知乎 》,目前也是处于手写阶段。因为,当有新的服务需要接进来时,要在网关层的代码中添加进去,当然,只要再写点儿代码,从一个外部文件中读取后端 GraphQL 服务列表,就可以将这个手写网关变成配置型的。

https://github.com/Jeff-Tian/serverless-space/blob/main/src/gateway/gateway.module.ts
image.png

总结

代理,或者网关,是个好东西,可以简化架构,但是别太多层代理了哦!