作者
一个 App + 一个 DB时代的终结
五年前,就如很多创业公司刚开始的时候一样,点融网的主要业务架构在一个被称为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的目标
- 兼容目前子系统的权限控制模型子系统的权限模型可以适配转换进入新系统的权限模型。
- 具有广泛的第三方鉴权系统或协议的可接纳性需要广泛的鉴权系统/协议的集成支持,包括但不限于:sso,oauth,ntlm/kerberos,openid,ldap/ms active directory,saml…因为说不清未来要集成什么东西。但有需要时可以通过配置,或以较小的代价接入。
- 具有良好扩展性扩展性表现在认证和授权的各个阶段和环节,每个环节都有默认实现,但可以提供途径根据需要进行覆写和干预。这通常发生在子系统认为框架提供的某个环节不爽的场景,比如我觉得公共登录页很丑,我要定制自己的登录页。
- 方便的组,角色,人员权限分配和控制当新加入业务操作人员时,管理人员只需要简短操作就可以方便的进行账户权限分配,控制,管理,权限开关设置等,
- 代码侵入性权限控制对系统业务级代码无侵入性,或有较少侵入性。
- 使用成熟解决方案,不重复造轮子使用业界久经考验的成熟开源方案,少些代码,遇到问题通过社区很快得到解决。
基于以上设计原则,我们选择了CAS + Spring Security的开源组合来作为我们Uniauth框架开发的基础、设计一套基于mysql的权限模型 将其融入到SpringSecurity中,为点融网的前后端分离进行了定制化,Uniauth雏形就有了。
架构设计
系统依赖架构图:
在这样的架构下, 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的客户集成起来要相对容易很多。
项目内部的模块依赖图:
源代码模块化, 比如
客户端(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的集成方的流程。
SSO纯API集成流程图:
(点击放大图像)
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²)的空间来存储关系,但现实中并不会需要这么多。
(点击放大图像)
数据结构关系:
每根红线都是comment_path中的一条数据, 线条上的数字为depth。
查询直接回复4号comment的comment(父查子)
select c.* from comment c join comment_path cp on (c.id = cp.descendant) where cp.ancestor = 4 and depth = 1;
查询所有回复4号的子comment(父查所有子)
select c.* from comment c join comment_path cp on (c.id = cp.descendant) where cp.ancestor = 4;
–如果你需要保留层级关系, 则将cp中的值也返回即可
查询所有7号的 父comment(子查所有父)
select c.* from comment c JOIN comment_path cp on (c.id = cp.ancestor) where cp.descendant = 7;
添加一条子回复到 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的节点.
从评论链中删除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号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优势分析
成熟度:
市场上会有一些开源的IAM系统。相较于很多项目在项目前期处于Bug较多的探索时期,UniAuth经过两年多的点融内部研发,和互金生产环境的检验,UniAuth已经是一个成熟的生产环境质量的产品。
成本:
相较于市场上一些IAM系统不菲的软件授权费用/授权使用费用,UniAuth将项目源码完全开源。并鼓励更多的极客可以贡献的代码,让很多公共模块可以投入更少,成效更快。
框架支持与拓展:
UniAuth的底层架构实现了SpringSecurity, 并且通过CAS实现Authentication, 因此可以支持包括SPEL在内的所有CAS, SpringSecurity的特性及其拓展。
数据库及设计:
对于早期的创业公司而言,成本控制永远是一个中心话题。基于MySQL的数据库,降低了很大的运营成本。并且,UniAuth的数据库设计,对于树状结构数据的增改,做了大量优化和特定设计。这会在后文提到。
客户端支持/跨域访问:
同时支持客户端和REST API访问。解决跨域访问问题。
SSO:
CAS天生自带SSO的实现, 为应用的Authentication提供更多的扩展可能性。
UniAuth 开源
随着点融的发展,点融技术部门也以更加开放的心态回馈社会,将UniAuth项目加入到开源项目社区。 https://github.com/dianrong/UniAuth
作者简介
钱晟龙,UniAuth项目组成员,点融网资深软件开发工程师,尝试设计并解决各类软件开发中遇到的实际问题,喜欢从已有成熟解决方案中找寻轮子,前端、后端、运维、沟通、管理、写运营稿,哪里重要去哪里, 现主要负责点融网SSO、OAuth2、JWT etc .. 认证授权相关功能。
转自 http://www.infoq.com/cn/articles/authentication-and-authorization-system-uniauth