简述
微服务下的用户系统从设计与传统单体应用是不一样的,传统单体应用下本质上用户系统是一个模块。用户系统是与整个应用紧耦合在一起的,具体来说,它们共享一套代码、一个数据库、通过代码级的API调用……
而微服务下的用户系统设计很不一样,因为微服务的特点,各功能都独立成一个Server在运行,那用户系统首先需要支持远程的API调用。基本来说,微服务下的用户系统设计需要满足以下要求:
- 独立的用户系统:用户系统应该是一个独立的应用程序,它不应该和业务系统紧耦合在一起。
- 用户认证、授权:用户认证、授权是用户系统的核心,可以基于 OAuth 2 协议来实现闪统一认证、单点登录。
- 单点登录:统一的用户登录入口。
- 接入应用管理:对接入用户系统的各个子应用,用户系统可对接入应用进行管理。比如:应用接口账户、应用SSO回调地址、应用用户管理……
- 丰富的API:微服务下各服务节点之间都通过API接口进行通信,支持RESTful风格的接口是最低要求,对于像用户系统这样的核心系统可以考虑使用protobuf、grpc这样的机制来提供传输效率。
- 丰富的功能:像组织管理、角色权限等功能,虽然不是设计用户系统的必要功能,等若能支持对于用户系统来说也是加分项。
- 高性能、高可用、可扩展
功能
基于以上考虑,用户系统计划实现如下功能:
- 用户管理:所有子系统不再有用户管理模块,统一到用户系统来实现。
- 统一认证:基于 OAuth 2 实现。
- 单点登录:可以采用 OAuth 2 的静默登录来实现。
- 应用管理:各应用接入用户系统,需要通过接口来操作用户,比如:创建、获取、遍历用户信息。同时对于单点登录、角色权限管理等功能也可以进行设置是否启用。
- 角色权限管理:应用可以选择将角色、权限上提到用户系统来进行统一管理,或者应用可以选择自己管理角色权限,只使用用户系统的统一用户认证和单点登录功能。
- 组织管理:对于企业级应用,组织架构管理是一个不可或缺的功能。用户系统支持树型机制的组织架构管理功能。
用户模块
用户模块本身并不复杂,主要需要规划好数据模型。在设计用户模型时可以充分利用PG的特性来简化用户模型。这里我们来看一个用户模型的示例:
1 | CREATE TABLE IF NOT EXISTS ig_passport |
可以看到,与用户模型相关的有两张表:ig_passport
和ig_app_user
。ig_passport
是实际的用户数据模型。
ig_passport
这里id
是用户主键,采用了 ObjectId 的十六进制字符串来表示。没有使用自增的整形字段来作为ID,主要考虑还是数据迁移的方便。为了 ObjectId 是一个分布式系统下的唯一ID,对于多个用户系统需要合并或拆分的情况,就不需要再去处理主键ID重复的情况了,同时,数据备份、恢复时也不会有自增序例不一至的问题。
其它字段都是用户表设计中常见的字段,这里就不再过多介绍。需要注意的是 data
字段,可以看到它的数据类型是:JSONB
,这是PG数据库特有的数据类型。它可以让我们在传统的SQL表里存储无模式数据,这里就是JSON数据。使用 data
字段的好处在,对于一些用户个性数据,比如:年龄、爱好、工作等,我们可以让前端将这些数据做为一个JSON类型提交上来。简单说就是,数据模型不限制应用可以上传哪些字段,由应用来决定。因为作为一个用户系统,它只需存储必要的字段即可,如id、登录账号名、密码、状态、权限等。用户的个性化数据可以待系统实现使用时由使用系统来决定,这样即可在不修改数据表(模型)的情况下做到用户模型的可扩展性。
对于需要通过用户个性化数据进行检索的情况,PG也支持对JSONB类型字段建立索引,可以选择对整个JSON字段数据建立索引,也可以选择对具体的key对应的值建立索引。
ig_app_user
ig_app_user
表是应用内用户的映射,对于某些已建应用集成到用户系统时特别有用。已建应用已经拥有了一套自己的用户体系,当它们集成到用户系统时,已用的用户体系是不能直接废掉的(或者需要一段时间的过滤)。这时候就使用 ig_app_user
来将应用已有用户和用户系统的用户进行映射了。可以看到,ig_app_user
里面有一个 user_id
字段,这就是应用已有用户的ID,可以通过 app_id
、passport_id
、user_id
三个字段来唯一确定一个应用内的已有用户。而 ig_app_user.data
字段也是JSONB类型,我们可以将应用原来用户表里的数据当做一个JSON格式数据存储在此,这样无论接入应用的原有用户表是怎么设计的,用户系统都可以在不修改表结构的情况下对其进行存储。
应用管理
用户系统需要管理多个接入应用的用户,甚至角色和权限也可以纳入统一管理。这首先需要一个应用管理模块,用于应用的创建、编辑、接入。应用模型可参考设计如下:
1 | CREATE TABLE IF NOT EXISTS ig_app |
其中几个关键字段说明如下:
- sso_redirect_uri:单点登录成功后重定向页面到应用的地址
- domain:是否校验请求应用的域名是否合法
- data_url:数据推送地址。当用户系统发生某些事件时将数据推关给应用。比如:用户创建、修改,应用创建、修改等。
OAuth 2
用户系统很重要的一个功能就是用户认证和单点登录,这两个功能可以使用 OAuth 2 协议来实现。这里,我们并没有使用现成的 OAuth 2 库或框架,而是基于 OAuth 2 协议规范 使用 Akka HTTP 实现了一套,因为我们不需要所有的 OAuth 2 功能,另外就是用户系统本身也是使用Akka HTTP 开发的,本着能造轮子就造的原则(正好,这玩意不太复杂,可以造)自行开发了一套。
注:之后会写一篇文章:《Scala实战:OAuth 2 服务》来介绍怎样使用 Akka HTTP 实现 OAuth 2
统一认证
OAuth 2 是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth 1.0。 OAuth 2关注客户端开发者的简易性。要么通过组织在资源拥有者和HTTP服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限。同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。2012年10月,OAuth 2协议正式发布为 RFC 6749。
Web ServerFlow是把OAuth 1.0的三个步骤缩略为两个步骤,首先这个是适合有server的第三方使用的。
OAuth 2认证流程
- 客户端通过HTTP请求Authorize
- 服务端接收到Authorize请求,返回用户登陆页面
- 用户在登陆页面登陆
- 登录成功后,服务端将浏览器定位到redirect_uri,并同时传递Authorization Code
- 客户端使用HTTPS发送Authorization Code
- 服务器端收到access_token请求,验证Authorization Code——生成access_token,refresh_token和expires_in(过期时间)——access_token和refresh_token和过期时间入库
- 返回access_token和refresh_token,expires_in(过期时间)
- 用户使用HTTPS协议,发送access_token及相应参数请求开放平台接口
OAuth 2除了支持浏览器端认证外,还支持APP、C/S客户端认证。
用户系统的单点登录
用户系统除了支持 access_token 这样的登录认证方式外,还支持传统的基于 session 的认证(session加密后通过cookie存储在浏览器端)。这种认证方式对于大部分基于Web的应用来说更熟悉,集成更方便。
它的区别在于 OAuth 2 认证的第4步,redirect_uri重定向回应用时直接带上 sessionCode ,这时客户端就可以通过设置 cookie 来将 session 存储到浏览器上来实现登录会话的保持。
静默登录
OAuth 2登录时可以选择静默登录模式。单用户已经登录过一次用户系统,他再访问另一个未登录的应用时,应用将页面重定向到用户系统的登录页面,这时候用户系统可以判断出已经有用户登录session,这时用户系统将直接重定向回到请求登录的应用并带上用户会话信息。一般,这是通过cookie来实现的。
有一个真实的案例来演示这一模式。你先登录新浪微博,这时会跳转到新浪通行证的登录页面。若你之前已经登录过新浪通行证,则回直接返回新浪微博并已设置为登录状态,否则会提示你输入用户名、密码进行登录。
客户端保存 session
通过cookie这样的客户端技术来保存session,服务端不需要记录session信息。这样,服务端重启不会影响session会话的状态,也不会造成session的丢失,同时,在服务端扩展时也没有session同步的问题。
但是,在某些业务场景下,服务端保存session是必要的。比如下一节讲到的统一登出和一次登录功能,还有当前登录用户数统计等。
统一登出与一次登录
一般情况下,将 session 通过 cookie 存储在浏览器客户端就可以满足用户会话保持的需求。但是,对于很多系统来说,都有统一登出和一次登录的业务需求。
- 统一登出:用户登出任一应用,则同时所有应用的登录状态都将无效。
- 一次登录:用户同一时段只能在一个IP地址登录(包括不同应用)。
要实现以上两个功能,就必需在服务端保存 session 。通过在每次收到论证请求时和服务端session数据比较来实现统一拿出和一次登录。除了在服务端保存session以外,对于接入应用也需要做一些改造。
统一登出
应用的Server端在每次收到客户端请求时,必需将sessionCode提交到用户系统进行验证。用户系统将返回些sessionCode是否有效。若用户已经在另一个应用上登出,则用户系统将返回此sessionCode已经无效的错误信息,这时应用将重定向请求到用户系统的登录页面或其它不需要登录即可访问的公共页面。
一次登录
若用户系统提示用户已经在另一个IP地址登录,则当前sessionCode被新的登录会话给挤掉(当前sessionCode不再指向当前登录用户ID)。这时应用应弹出会话失效提示框告知用户,再跳转到可公开访问页面(若无,则跳转到登录页面)。
角色权限
角色权限,应用可以选择自己管理,也可以选择由用户系统来管理。
在角色权限功能的设计上,我们可以通过两个基本的抽象来实现:
- Resource:资源。资源可以为应用系统的功能权限、菜单权限、接口权限、按钮权限等,在用户系统里统一将此类控制相关的权限称为 资源。
- Role:角色。角色就是拥有一系列资源的集合体,通过角色这样的概念来命名并使用。
有了资源和角色,我们就可以实现复杂的权限控制功能。
组织管理
组织在企业级应用中非常重要,通常来说组织都需要作为一个树型结构来设计。组织在这里可以代表企业法人、事业法人或政府部门等。
比如在政府行业:
1 | 有市级组织 --> 市发改委 --> 市发改委科室 |
组织模型的设计,可以把 parent
和 parents
两个字段加到表字段中。parent
是直接父组织ID、parents
是完整父组织ID列表:
1 | CREATE TABLE IF NOT EXISTS ig_org |
利用PG的 数组字段类型 使用一个字段就可以存储组织的完整父ID列表。一般数据库需要使用关系表存储或需要把数组按分隔符连接成字符串来存储,使用关系表需要多维护一张表,对工作量有所增加,而且类似的关系表过多有可能会造成表爆炸;而使用分隔符连接的字符串对查询不友好……
单需要获得某个组织的所有子组织(包括间接子组织时),使用类似如下sql即可:
1 | select * from ig_org where '<org id>' = any(parents); |
技术
Server
用户系统在微服务架构设计中是一个核心系统,它必需具有高可用、高性能、可扩展。这里,我选择了使用 Akka HTTP 来进行设计。使用 Routing DSL 可以很灵活的设计出用户系统的 RESTful 接口。Marshalling和Unmarshalling可用来对数据模型与HttpRequest、HttpResponse做高效的相互转换。
Akka HTTP完整的支持HTTP/HTTPs,同时也支持 HTTP 2,它底层基于 Akka/Akka Stream 实现。具备高可用、高性能、可扩展,还拥有容错、集群等特性。同时,Akka做为一个库,而非框架。不像 Spring 那样“巨重”,它足够轻量,很适合用来实现一个微服务。
Storage
用户系统使用 PostgreSQL(以下简称:PG)数据库来做为数据存储,PG 10原生已经支持表分区、逻辑复杂等功能,具备良好的横向可扩展性。同时,PG丰富的数据类型可简化我们的数据模型设计,如:JSONB、Array……
总结
在微服务下设计一个用户系统,需要考虑到各接入应用都是独立的服务,在系统设计之初就要考虑到各功能模块的可扩展性。统一认证和单点登录做为核心,
可以采用 OAuth 2 协议来设计。
技术架构上,Akka HTTP可作为实现微服务的一套强有力的工作库。具有异步、高性能、集群管理等特性。