背景 : 答主之前在某公司开发过一个多租户场景下极其复杂的需求。需求一开始听起来简单, 但实际开始着手后, 无论是产品设计,还是开发阶段,还是测试, 运维阶段,都充满了坑,可以说是牵一发动全身的大需求,完美展示了 为了加个小框框,需要对一堆屎山进行重构 。甚至到快离职时,依然爆出来零零散散的问题。其根源在于已有的SAAS系统中,租户之间数据并没有彻底的被隔离。 此文不讲任何技术上的细节,只讲产品,讲设计,帮助大家以后开发多租户系统尽量避免一些大坑。
需求背景
现有产品背景
先来了解下现有的产品情况:
- 现有产品支持3种模式部署,单租户私有化部署(一个客户公司就是一个租户,服务全部部署在客户公司内部,一般适合中小型公司),多租户私有化部署(客户公司可以自行创建租户,用户在不同的用户下使用,一般是一个大部门或者一个分公司位于一个租户下,所有服务部署在客户公司内部,一般适合大公司,大公司内部组织较为复杂有子公司分公司或者大型部门的概念),SAAS部署(每一个客户的公司对应一个租户,来一个新公司就注册一个租户,所有服务部署在我司的机房,一般适合小公司,客户公司可以省去机房和服务器开销).如果是做toB产品的同学,应该对这几种部署方式不会陌生
- 公司产品支持多种认证协议登录,每一个租户都可以配置自己的认证协议,不同租户的认证协议相互隔离。最常见,用得最多的 就是用户名-密码认证协议. 管理员可以在 认证源配置页面对租户下的用户使用哪种认证协议进行配置。
- 单租户私有化部署模式下,会自动创建一个 默认用户-密码认证协议 ,多租户下部署(包括私有部署和SAAS部署)下,会自动创建一个被所有租户共享的 默认用户-密码认证协议。
- 若用户忘记了密码,可以通过邮箱认证码的对密码进行重置
- 发送邮件的配置信息位于认证服务(auth server)的配置文件里面
- 邮件的配置被所有租户共享,也就是所有租户下的用户如果忘记密码,需要发邮件验证码,都是同一个配置,包括邮件标题,邮件内容模板都是一样的。
认证源配置详细说明
下面这幅图详细说明了不同部署模式下的认证源是如何存在的。 不同租户都可以随意选择自己的认证方式,在多租户场景下所有租户都共享一个 默认用户名密码认证源,之所以这样是为了在创建租户时确保每一个租户都有一个默认的 认证方式,用户直接可以登陆我们的系统,后面会说到这个设计是有大坑的。
在数据库中, 认证源表是这样存在的:
bash
auth_source_table
id | display_name | protoc_type | conf | company_id | is_init
1 | 默认用户名密码协议 | usernane_pwd | "" | -1 | true
其中 id 为自增主键, display_name 为认证源的名称, protoco_type 为协议的类型,conf 为协议类型的配置(当前协议不需要,但别的协议可能需要配置一些参数), company_id 为租户id, 如果是单租户部署为0, 多租户部署从1开始自增,-1 表示当前这个认证源被所有租户共享,is_init 为 true 表示由系统创建,而不是手动创建,为 true 的情况下前端不允许管理员对其进行编辑。
产品提出的需求
产品的需求非常简单, 就一个需求:
- 有个客户公司不想用邮件发送重置密码的验证码,他们想用手机短信发送重置密码验证码,手机短信需要用客户内部协议,需要对接他们的短信协议。一句话,就是用客户的短信协议,发送重置密码的验证码
仔细思考一下这个需求
怎么样,是不是听起来很简单? 的确,从完成功能的角度来说,确实很简单,后端对接一下短信协议,把手机短信协议的配置写在配置文件里面; 前端把邮件验证码的框框换成手机验证码的框框就可以了,简简单单对吧。
你这样想就是土养土森破了。 我可以随便问几个问题,你怎么解决:
- 明天又有一家公司来了个新的短信协议也要求用它来重置密码你怎么办? 往配置文件上一直堆?
- 多租户系统,只有一个租户使用短信协议重置密码,其他都是邮箱协议重置密码,你怎么区分,还写在配置文件里面? 一个系统有100个租户,你怎么办?
- 即使你通过某些方式写在配置文件里面,你配置文件很小很清晰,那么假设客户短信协议换参数了你怎么办,要求重启服务吗?一个租户换了协议参数,你总不能把所有租户的 auth server 都去重启了吧?
看到这里,好像发现了现有产品的几个坑:
- 忘记密码只能通过发邮件进行密码重置,不够灵活,不能使用发短信进行认证
- 邮箱重置密码的配置位于配置文件,所有租户共享一个配置,不好更改,比如A公司想用一个邮件模板,B公司想要另一个邮件内容的模板,众口难调无法适配所有公司。
- 如果要更改,需要重启服务,如果是SAAS模式部署这将不好搞了。可能A公司的人正在办公,你卡嚓重启,人家直接登不进系统了. 即使你的服务支持热修改,但要是你的配置修改错了呢, 也有可能造成用户的不可用
初步的想法
基于上面的分析,我们很自然有个初步想法
- 一定要把重置密码的邮箱配置从配置文件中剥离,放在配置文件里面实在是太不灵活了
- 把重置密码的邮箱配置放到认证源中,以认证源绑定认证源的方式管理 重置密码的认证源。这么说有点绕,就每个租户下面的每一个 用户名密码认证 认证源 都可以选择绑定自己的重置密码方式,你想用邮箱就用邮箱,想用短信就用短信, 完全随意。
详细设计
基于上面说的两点我们来看看这个小小的需求,后端到底要做哪些改造
-
新建一张表,用于绑定 重置密码的认证源和 用户名密码认证源 的绑定关系。 纳尼?一上来就多了一张表?没错,这张表还非加不可, 因为要记录的东西还不少。表结构和数据长这样(以多租户部署为例):
bashpwd_rst_table id | username_pwd_id | reset_id | company_name 1 | -1 | 3 | 1 2 | -1| 4| 2 3 | 2 | 3 | 2 4 | 2| 5| 2 auth_sorce_table id | display_name | protoc_type | conf | company_id | is_init 1 | 默认用户名密码登陆| username_pwd | "" | -1| 1 | 2 | xx集团用户名密码登陆| username_pwd | "" | 2| 0 | 3 | 邮箱验证码重置密码 | mail| "xxxxx" | 1 | 0 4 | 邮箱验证码重置密码| mail | "xxxxx" | 2 | 0 5 | 短信验证码重置密码 | xx_phone | "xxxxx" | 2 | 0
pwd_rst_table 一张关联表, 其中的id为自增id, usernane_pwd_id 和 rst_id 均为 auth_source_table中的 id. 这张表记录了每个租户下的 所有 用户名-密码认证源(protoc_type为username_pwd的认证元) 与 哪些 用于重置密码的 认证源之间的 绑定关系
上面表格对应的绑定的关系就像下面这张图这样:
-
因为有了上面这张表这个拖油瓶, 所以以往的 认证源管理的 所有接口(增删改查) 都需要重新调整, 这里的工作量非常巨大 且 全是特殊逻辑 ,比如以前删除某个认证源 你直接就可以删除,但现在不行了,你看一下他有没有绑定关系,如果有绑定关系那么要把绑定关系删除;或者这个 认证源被用来 重置密码,那么这个认证源压根就不应该被删除; 又或者这个认证源 被用来 重置密码(邮箱协议), 我想把它改成别的类型(图片验证码),如果 这个认证源已经有绑定关系了,这显然是不能被更改类型的; 又或者我创建了一个 用户名密码的 认证源,那么必须要有一个重置密码的认证源与之绑定,否则不予创建。 诸如此类的,可以看出多了一个绑定关系以后,需要处理的特殊场景非常之多。
-
做到用户无感知升级。 上面说过,我们的把重置密码的邮箱配置从 auth server 的配置文件中删除了, 把它挪到了了 认证源 管理页面中图形化操作中。那么用户体验的一致性,我们对已有的客户或者是租户 得做数据迁移,总不能把服务升级上去,以前可以用邮件重置密码,升级上去就使用不了了吧。 所以每次重启服务就得从配置 文件中读取 邮件配置(有些客户的邮件配置可能不存在,因为他们不用 用户名密码登陆,本来就不需要这个配置), 为每个租户尝试 创建创建一个 邮箱验证码认证源, 并将这个 认证源绑定到所有 不存在 绑定关系的 用户名密码 认证源上面去。 是不是听起来很晕? 如果有流程来表示,就是下面这样:
简单起见,我就只画了多租户(包括私有化和saas部署),单租户部署就无需最外面那层遍历。
整个过程用一句话说 就是不断的遍历租户,遍历租户下的 用户名密码认证源,尽一切可能确保每一个 用户名密码认证源都有自己重置密码的认证源, 尽最大可能确保用户在忘记密码的时候还能够通过验证码来重置密码。
等等,还有坑没填
在我快离职的时候,运维还爆出来了一个安全性 漏洞, 这个漏洞其实从产品设计上很难避免。
在之前的版本中,邮箱的配置是在配置文件中的,只有运维可以看得到,在saas部署中,每个租户的管理员是看不到邮箱配置(有自己公司的邮箱端口,用户名,密码等,外泄可能造成发送攻击者以公司名义恶意邮件)的,只有我司的运维可以看得到。但在新版本中,auth server 会把读取配置文件中的邮箱配置,然后为每一个租户创建一个邮箱找回密码等认证源,邮箱的配置可以通过可视化的方式被每个租户管理员查看到。也就是说我司的邮箱配置,可以被每个租户下的管理员看到。
仔细想想,这个漏洞其实是无法避免,或者无法根除的,既然要把配置文件中的配置可视化展示出来,那么必然会被看到。但既然有漏洞,我也不能放置不管,只能做一些临时的补救止损措施。我的解决方案是将从配置文件读取出来的邮箱认证源的某些字段进行脱敏处理,全部以" *************** " 的形式返回, 编辑的时候,如果敏感字段仍为" *************** ", 则不对数据库的敏感字段进行写入操作。相当于对敏感字段进行特殊处理。
总结一下
小小的贡献
总结一下,这个看似简单的小需求,我做了哪些工作
- 将配置文件中的重置密码的邮箱配置 转换成 认证源的 方式,方便可视化,自由更改
- 不同的租户可以自由的选择重置密码的 方式,可以是邮箱验证码,也可以是短信验证码,且实现 重置密码方式的租户隔离
- 为了兼容老版本,为了客户无感知升级,我会为每个租户都创建一个邮箱重置密码的认证源,并将其绑定到所有 没有绑定重置密码认证源的 用户名密码认证源 上面
- 因为引入了一个 自动创建的 邮箱认证源,所以 认证源的 增删改查操作需要特殊处理,里面有大量的隐藏特殊的约束
- 因为把配置文件中的 邮箱配置 可视化了,造成了 邮箱配置被租户的管理员看到,有一定安全风险,需要对敏感字段进行 脱敏处理
经验教训
- 对于产品的新特性, 最好做到无感知升级,这里的无感知是针对用户,也是针对管理员,也是针对运维,尽可能避免手动操作数据库,手动跑一些小工具之类的
- 对于产品的新特性, 最好做到无感知升级,这里的无感知是针对用户,也是针对管理员,也是针对运维,尽可能避免手动操作数据库,手动跑一些小工具之类的
- 开发一定要考虑运维同事的感受,尤其是tob系统, 很多私有化部署的场景,你去客户现场改个小配置可能需要填好多表格,等客户领导层层审批, 所以一切设计都要尽可能实现自动化操作,避免给运维同事带来麻烦
- 租户之间数据一定要彻底隔离, 尽量避免一个 数据 被所有租户共享的情况,宁愿为每一个租户单独创建一条数据,也不要共享数据。 可以看到这个开发的大量工作都用在了处理特殊逻辑上面,都用在了租户之间的数据解耦,数据隔离上面,这都是一开始的前人给后人埋的大坑,要花数倍的时间去填
- 安全无小事,任何和密码相关的操作都要谨慎,谨慎,再谨慎。一定要明白哪些数据可以给用户看,哪些不可以给用户看,给用户看了风险是什么。
- 作为开发,一定要多给自己预留多点时间,设计,开发,测试,上线都需要冗余度。 产品画个ppt就可以交差了,但后续的升级,修漏洞,维护都是要由开发来负责兜底的,所以开发一定要多给自己预留多点时间。