软件设计中最重要的莫过于对于架构的设计。所以项目伊始,甩手将军对项目的静态以及动态架构有过许多考量。
静态架构
静态架构表达了软件在非运行状态下的架构,也就是“纸上的架构”,它通常表达了软件各个模块在编译时期的依赖关系。由于我们定下了“要构建可供社区复用的SDK”的目标,对于OAuth和Open ID Connect功能的实现不能直接放在同一个模块里,而是要抽离出来成为独立的子项目。对于这样的要求,我们把整个项目分为三个部分:
以OAuth SDK子项目为核心
Open ID Connect SDK子项目提供对于OAuth SDK的拓展功能
API调用两个SDK
其中,因为OAuth SDK是核心组件,对这个组件内部的架构,我们也要有一个初步的设计。研读文档并借鉴其他设计后,甩手将军将OAuth的处理分成了五个部分:
对于HTTP请求的抽象。在获得HTTP请求后,我们需要把这个请求抽象成对于OAuth处理有用的结构,而不是直接把HTTP请求对象丢给下游去处理。另外,SDK不应对用户使用哪个HTTP标准库产生限制,所以对于HTTP请求对象本身,我们也应该做一些抽象:只通过抽象接口获取HTTP数据,把提供数据的任务留给用户。
对于请求的验证。在开始处理前,我们需要对请求本身进行一系列验证。这个操作对应了标准文档中那些对于MUST、MUST NOT、SHOULD、SHOULD NOT的定义。只有保证满足了这些定义,我们才能够放心安全得处理OAuth请求。
OAuth处理器链。这个阶段是对于标准文档中各个不同的处理流程的实现,是整套SDK的核心。通过链表的方式,我们让OAuth请求逐个通过为不同功能设计的处理器。如果一个处理器能够处理这个请求,变可以对上下文的数据进行增添或者修改。比如,和分别实现了OAuth授权码流程(Authorize Code Flow)中的部分功能。那么,在处理一个OAuth授权码请求的过程中(特征为或者),它们就会参与进来。前者会在(首次)请求时生成授权码,后者会在(后续)请求时生成刷新令牌。
令牌算法。标准文档中并未详细阐述令牌和授权码具体是如何生成的。对于用户来说,它们只是一个近似乱码的字符串。在业界流行的实现中,也可以把它们实现为JWT(JSON Web Token)的形态。作为一个SDK,不应该提前为用户做决定,而是应该通过抽象的方式,提供不同的实现让用户挑选。这个部分,即是对不同的令牌算法而做的抽象。
令牌会话管理。这个部分是对令牌算法的衍生。一些OAuth流程涉及多次请求(比如授权码流程、刷新令牌流程),服务器需要在用户多次请求之间,保管会话的状态,以便在再次接到请求后进行处理。最简单的方式,便是把这些数据全部扔进数据库,下次请求发生时再读取。但如果令牌是以JWT的方式实现的,所有的会话信息都已经录入到分发出去的令牌中了,下次用户拿令牌来交换的时候只需要读取令牌中的信息即可,不用再去数据库。显然,对于会话的管理在一定程度上也是因令牌算法而异。所以,类似对于令牌算法的抽象,我们对于令牌会话的管理也要做出抽象。
拓展的代价
OAuth规范中定义了很多常量。比如,code代表授权码、token代表访问令牌。Open ID Connect规范中对部分常量进行了拓展。比如,在code和token之外,又定义了id_token代表身份令牌。
对于常量的处理,一般我们会选择采用枚举类型进行实现。然而,Java中的枚举类型不允许子类进行继承,这否定了我们对于采用继承来实现拓展的想法。由于我们很坚定得要把OAuth SDK和Open ID Connect SDK分开处理,由此便不能采用枚举类型来代表这些规范中的常量,而不得不采用弱类型的字符串常量。
而对于“拓展”的表达,则只得尽量在常量命名上进行体现。
动态架构
静态架构之外,我们更加关心系统的动态架构。这是系统在运行时,各个模块之间的逻辑关系。这是看不见也摸不着的,所以也可以理解为“脑子里的架构”。
甩手将军深信一个道理:架构是演变出来,而不是设计出来的。首先,一个每日访问量只有几百的系统显然没有必要采用高可用、易伸缩的架构。而一个每秒访问量上千的系统,如果还想着用单个服务搞定所有用户,那简直是在做梦。在需求不明确的情况下,我们没有必要一上手就是各种微服务架构。况且,这非常考验团队的集成能力,在迭代式的开发过程中,很容易遭致各子系统之间的混乱(对于像甩手将军这样单人进行开发作业,更是容易导致精神分裂)。反之,如果一开始迅速以单个系统的方式完成了实现,再慢慢将部分功能剥离出来形成独立的子系统,这样的演进方式往往靠谱的多。
基于这样的认知,最初开发的版本,是最最传统的单服务、单数据库架构。Astrea Server作为一个独立的服务,使用Redis数据库来存储数据。并通过HTTP调转的方式依赖外部的用户验证和用户授权。开发的时候,我们可以使用之前开发的OAuth SDK和Open ID Connect SDK,并利用Spring Boot来迅速开发出一个可用的原型。这个原型是可以在低并发场景下(比如公司内部)使用的。
然而,我们的终极目标并不止于此,既然要用这个项目实践微服务的设计及开发,那么在没有明确用户流量的情况下,我们不妨假设一下高流量带来的高可用性以及易伸缩性等需求,来想想如何“演进”项目的架构。
在上面这个传统架构下,我们认识到几大问题:
单个数据库本身可能成为瓶颈及隐患,它管理着几大不同的数据(授权码、授权会话、刷新会话等等),对应的是不同的OAuth流程。单个流程的流量可能通过数据库这个瓶颈,对其他流程产生影响。
Astrea Server本身身兼多种流程处理。在设计上,不同流程所获的的计算资源是平等的。而在实际运行时,并非如此。对于一个客户来讲,授权只做一次,接下来刷新可能两周一次,从访问频率上看很显然是不等的。更不要提使用不同流程的客户数量一定是不一样的,在手机端应用暴涨的情况下,隐式流程的访问可能大大增加,而这却不会对刷新流程产生任何影响,因为隐式流程是不允许申请刷新令牌的。
这样单服务的设计,注定了服务本身只能有“在线”和“下线”两个状态,并不存在“部分在线”这样在系统故障时仍能继续提供部分服务的状态。
程序内部逻辑过于复杂,不利于后期进行维护升级。用户验证和用户授权这两个动作,是OAuth操作的前提,并不属于系统内部逻辑,然而在单服务的设计下,程序仍然要花费相当篇幅对此进行处理。
基于这样的认知,甩手将军又设计了另外一版的多子系统架构,来弥补单系统下的不足。
在这个设计下,总网管负责将OAuth定义的两个HTTP路径分别倒流到授权端口网关服务(Authorization Endpoint Gateway Service)和令牌端口网关服务(Token Endpoint Gateway Service)。其中,授权网关负责和外部的用户服务进行交互,并提前获取用户的验证授权信息。在初步的请求验证后,即将请求分发给后台能够处理授权端口请求的流程服务。而令牌网关则负责对客户进行验证,并在验证通过后把请求分发给后台能够处理令牌端口请求的流程服务。
后台的流程服务在各自独立存在:
授权码服务完整负责OAuth/OIDC Authorization Code Flow,并自主保存会话信息。
混合流程服务完整负责OIDC Hybrid Flow,并自主保存会话信息。
隐式流程服务完整负责OAuth/OIDC Implicit Flow,因为不用管理会话,它是个无状态服务。
客户凭据服务完整负责OAuth Client Credentials Flow,也是个无状态服务。
刷新令牌服务完整负责在访问令牌或身份令牌过期后,客户持刷新令牌再次获取新的令牌。
网关服务和流程服务之间,采取GRPC通信,以降低HTTP JSON API带来的数据传输延迟。整个系统中,虽然网关服务占据了数据的进出口,但因为它们是无状态的,这意味着我们可以根据流量的大小来伸缩这个服务,使其不会成为系统的瓶颈。另外,因为各个流程都是独立运行,一个流程服务的问题不会影响其他流程,它们可以独立运营、独立升级。最后,因为数据库也是流程服务本地所有,所以单个数据库服务不会成为整个系统的瓶颈。
并且,现在我们可以更加自由地根据存储数据的不同选择不同的数据库。比如,授权码是短期交换的令牌,我们大可不必真的把它存进硬盘,使用Redis DB存进内存即可,就算丢失了也不是什么天塌下来的事情,程序可以再次获取,我们可以以绝对可靠性的代价换取一些性能。而对于刷新令牌而言,作为已经发出去的正式令牌,作为对用户的承诺,应当好好把它存进硬盘里,那么Redis则未必适合。
甩手将军最后要用一个安利来结束2018年的更新。
对于IT工作者来讲,每天面对显示器的时间基本占据了生命的1/3到1/2,眼睛已经相当疲劳。而传统台灯射出的光源,打在屏幕上,反射回眼睛,不但照明效果不佳,反倒增加了眼睛的负担。在工作台上,拥有一款能照亮显示器内容、又不伤眼的灯源,对于眼部健康至关重要。
明基ScreenBar,是甩手将军最近发现的一款屏幕挂灯。
它搭载在屏幕上方,采用垂直照射的方式,不但打亮了屏幕内容,而且避免了对眼睛的各种直射反射。甩手将军在两周的使用中,对这个挂灯爱不释手,故此也推荐给大家。
甩手将军 2019再见
领取专属 10元无门槛券
私享最新 技术干货