年前,就如很多创业公司刚开始的时候一样,点融网的主要业务架构在一个被称为MainApp + Workflow上的应用:
MainApp处理投资者的投资、充值、提现等投资端的操作;
用Workflow来处理所有的进件、审批、放款、催收等贷款端的操作。
当业务体量并不大的时候,世界一切都显得那么简单。前面一个包含了MainApp + Workflow的应用,后面一个包罗万象的数据库。一切为了快速的迭代和发展!彼时那是一个 App + 一个 DB的时代。
斗转星移,点融网渐渐从一个小荷才露尖尖角的小松鼠,发展成一个互金领域的映日荷花别样红的强力领军人物之一。到这个阶段,很多公司都会面临业务的复杂度极具上升,需要通过分拆业务系统来承载更多的业务流量及复杂性。于是很自然的,前边的应用,从1变成了N。点融的世界开始变得不是那么简单。我们的工程师面对着N * App 的场景。每个应用都在做自己的权限控制模块。因为,似乎每个应用的服务对象都不尽相同。有销售、有运营、有技术支持、有财务审计等等。
所以,谁/何种角色,对于某些特征资源拥有怎样的访问权限?这是萦绕在很多点融工程师心中的一个问题。
新的突破:认证和授权功能- UniAuth
两年前,几位点融的工程师,被赋予了这样的使命:以较小的代价实现中立的,脱离于特定业务场景的认证(authentication) 和授权(authorization) 功能 – UniAuth。
摆在这些工程师面前的有这样一些问题:市场上是否有类似的系统可以被使用呢?
UniAuth 这种类型的系统,在市场上叫做 IAM (Identity Access Management) 。UniAuth 既是以做轻量化的 IAM为目标。 这方面在美帝做得最好的公司叫做 okta(15年估值15亿美金) , amazon 的 AWS 里面也有 IAM。
美国的公司,对于国内有墙,服务器不稳定的可能性、对中国 support 不好 这些因素导致我们很难选择美国的 IAM产品,并且这些服务,本身的资费对于一个处于成长期的公司而言太贵,同时对于互金类公司而言,对于数据/系统的安全性的考量,我们会更倾向于基于框架上的二次开发模式。纵观当时市场上轻量级的IAM 系统, 要么没有开源满足点融需求可用的、要么仅存在于精美的PPT当中。
基于成本的考量,当时的点融还没有相应的预算投入到这些在业务价值中的优先级较小的项目中。基于定制化需求的考量自己公司做一个,持续投入资源在 support 上形成优势。UniAuth 的目标:
1)兼容目前子系统的权限控制模型
子系统的权限模型可以适配转换进入新系统的权限模型。
2)具有广泛的第三方鉴权系统或协议的可接纳性
需要广泛的鉴权系统/协议的集成支持,包括但不限于:sso,oauth,ntlm/kerberos,openid,ldap/ms active directory,saml...因为说不清未来要集成什么东西。但有需要时可以通过配置,或以较小的代价接入。
3)具有良好扩展性
扩展性表现在认证和授权的各个阶段和环节,每个环节都有默认实现,但可以提供途径根据需要进行覆写和干预。这通常发生在子系统认为框架提供的某个环节不爽的场景,比如我觉得公共登录页很丑,我要定制自己的登录页。
4)方便的组,角色,人员权限分配和控制
当新加入业务操作人员时,管理人员只需要简短操作就可以方便的进行账户权限分配,控制,管理,权限开关设置等。
5)代码侵入性
权限控制对系统业务级代码无侵入性,或有较少侵入性。
6)使用成熟解决方案,不重复造轮子
使用业界久经考验的成熟开源方案,少些代码,遇到问题通过社区很快得到解决。
基于以上设计原则,我们选择了 CAS + Spring Security 的开源组合来作为我们 Uniauth 框架开发的基础、设计一套基于mysql的权限模型将其融入到 SpringSecurity 中,为点融网的前后端分离进行了定制化,Uniauth 雏形就有了。
架构设计
1)系统依赖架构图
在这样的架构下,cas 服务器独立地存在,拥有了扩展 uniauth-server 数据源的可能性,可以同时 从 LDAP 取得企业用户数据,也允许客户端以纯 webservice 的形式集成到 Uniauth 当中。在authentication、authorization、data level filter 的整个链条中,每一个链条都可以被打断。
举例来讲,我们来了一个python base 的客户,他只想要做 authentication,那么在上图中,只需要通过 cas 完成 authentication拿到用户的 identity,然后再使用 identity 去 uniauth-server 得到该 identity 的 profile即可满足其需求,整个流程全部以 webservice 的形式完成, 与客户端语言无关。
再举例来讲,数据系统想要数据,直接问 uniauth-server 的 API 索取即可。当然默认情况下我们提供了强大的基于 SpringSecurity 的 CAS 客户端,内网 Java base 的客户集成起来要相对容易很多。
2)项目内部的模块依赖图
源代码模块化,比如客户端(uniauth-server的)如:业务系统、数据系统仅依赖 common 模块即可访问 uniauth-server 的读接口,并且尽可能少的依赖 jar包。客户端(uniauth-server的) 如:techops、cas 仅依赖 share-rw 即可访问 uniauth-server的读和写接口。同时 uniauth-server 提供的接口是基于 jax-rs 标准的 json 接口,所以异构系统完全可以自己写客户端访问uniauth-server。
集成系统集成 Uniauth 框架图:
该图描述了一个以正常形式集成 Uniauth 系统的 java base 的集成方的流程。
Uniauth 提供了纯 API 的形式集成 Uniauth 的系统,该方式给予了客户最大化的自由,不想使用我们提供的任何 jar 包,或者异构系统。集成想要自由,就给它自由,而且自由的同时保证了认证机制的安全。
数据库模型图(在没有加上 SaaS 化之前的版本):
现在的数据库里面对于 User 表又扩展了其 EAV 模型,增加了 SaaS etc..几个比较重要的点:
1.所有的实体数据都不会被删除,只会被禁用(status 字段)
2.角色可以通过组赋予与集成、也可以直接赋予人
3.组树状结构的 closure table 设计
4.不同的 domain 拥有不同的 role 和 permission,权限数据在不同集成系统之间隔离,而 user 和 group 数据又是共享的
5.audit 表通过 aop, 记录下对数据库和 API 的一切访问轨迹
6.树状数据库设计
为了避免过长的篇幅描述 UniAuth 中多个树状数据结构,以一个网易评论树来做讲解:
UniAuth采取了闭包表的数据设计方式:
Comment Table Data:
Comment Path Table Data:
这种设计,comment table 本身并不保存评论与评论之间的关系,而将该关系用另外一张表(comment_path)保存起来,理论上讲需要 O(n²)的空间来存储关系,但现实中并不会需要这么多。
3)数据结构关系
每根红线都是 comment_path 中的一条数据,线条上的数字为 depth。
1.查询直接回复4号 comment 的 comment(父查子)
select c.* from comment c join comment_path cp on (c.id = cp.descendant) where cp.ancestor = 4 and depth = 1;
2.查询所有回复4号的子 comment(父查所有子)
select c.* from comment c join comment_path cp on (c.id = cp.descendant) where cp.ancestor = 4;
如果你需要保留层级关系,则将 cp 中的值也返回即可
3.查询所有7号的父 comment(子查所有父)
select c.* from comment c JOIN comment_path cp on (c.id = cp.ancestor) where cp.descendant = 7;
4.添加一条子回复到 6号 comment 上(新增)
step a:
insert into comment(value, topic_id, user_id) values('(10)我以gin食阼啦', 1, 2);
拿到该句返回的 id, 假设为10
step b:
insert into comment_path (ancestor, descendant, depth) select cp.ancestor, 10, cp.depth+1 from comment_path as cp where cp.descendant=6 union all select 10, 10, 0;
只要拥有子 comment_id 为6作为子节点的节点,全都新增一个 id 为10,depth+1的子节点 并且插入一个10, 10, 0的节点
5.从评论链中删除4号 comment 及其子 comment(删除子或者子树)
delete a from comment_path a join comment_path b on (a.descendant = b.descendant) where b.ancestor=4;这句话等价于"delete from comment_path where descendant in (select descendant from comment_path where ancestor = 4);”
但 mysql 会报 from 句子中的表不能用于 update
6.将6号 comment 的父 comment 更改为2号(移动子或者子树)
step a:
delete a from comment_path as a join comment_path as d on a.descendant = d.descendant left join comment_path as x on x.ancestor = d.ancestor and x.descendant = a.ancestor where d.ancestor = 6 and x.ancestor is null;
这样删除的原因和需求5一致
step b:
insert into comment_path (ancestor, descendant, depth) select supertree.ancestor, subtree.descendant, supertree.depth+subtree.depth+1 from comment_path as supertree join comment_path as subtree where subtree.ancestor = 6 and supertree.descendant = 2;
以上6种需求覆盖了最为常用的几种情况,解决了基本上 UniAuth 在闭包表上遇到的所有的问题。
closure table 是反模式设计的一种经典设计,在结构化数据库里面,SQL 可以很轻易高效地支持对树的各种各样的增、删、改、查、移的需求。这种设计给到 UniAuth 系统中的数据表设计特点给了很大的支持。
UniAuth 优势分析
1)成熟度
市场上会有一些开源的 IAM 系统。相较于很多项目在项目前期处于 bug 较多的探索时期,UniAuth 经过两年多的点融内部研发,和互金生产环境的检验,UniAuth 已经是一个成熟的生产环境质量的产品。
2)成本
相较于市场上一些 IAM 系统不菲的软件授权费用/授权使用费用,UniAuth 将项目源码完全开源。并鼓励更多的极客可以贡献的代码,让很多公共模块可以投入更少,成效更快。
3)框架支持与拓展
UniAuth 的底层架构实现了 SpringSecurity,并且通过 CAS 实现 Authentication,因此可以支持包括SPEL在内的所有 CAS,SpringSecurity 的特性及其拓展。
4)数据库及设计
对于早期的创业公司而言,成本控制永远是一个中心话题。基于 MySQL 的数据库,降低了很大的运营成本。并且,UniAuth 的数据库设计,对于树状结构数据的增改,做了大量优化和特定设计。这会在后文提到。
5)客户端支持/跨域访问
同时支持客户端和 REST API 访问,解决跨域访问问题。
6)SSO
CAS 天生自带 SSO 的实现,为应用的 Authentication 提供更多的扩展可能性。
UniAuth 开源
随着点融的发展,点融技术部门也以更加开放的心态回馈社会,将UniAuth项目加入到开源项目社区。
点融开源社区:https://github.com/dianrong/UniAuth
本文作者:钱晟龙 Arc_Qian(点融黑帮),现任点融网架构组产品研发工程师,主要任务是思考并尝试解决各类点融网迈出第一公里之后遇到的现实问题。