网关(gateway)——不一样团队子系统登陆验证问题总结

1. 需求分析

1.1 背景介绍

  一个主系统和若干其余团队(或公司)开发的子系统(主系统和每一个子系统都有先后端)须要对外展示为一个系统(实际上没有主系统和子系统之分,这里是由于主系统研发团队须要对整个系统负责,而子系统研发团队只对自身系统负责)。因时间关系,主系统研发团队再也不从新开发界面,而是直接跳转到子系统界面。这样的话,就至关于一个系统有多个界面和多个后端。通常校验用户是否登陆都是在各自的后端完成的,而多个子系统的话就会有多个帐号体系,这对于系统融合就很麻烦了。这里为了简单,要求各子系统自己不作用户登陆态校验,统一由主系统研发团队负责用户登陆态校验工做。html

1.2 需求具体化

  1. 以下图所示,咱们由一个主系统和两个子系统,这里须要将这三个系统对外展示为一个系统。

    09-46-15.jpg

  1. 因为时间关系,主系统研发团队不开发包含子系统功能新界面,而是直接跳转至各子系统界面,也就是说各子系统web端只能访问自身的子系统后端服务,各子系统后端服务之间也不能互相通讯。因此需求进一步细化,以下图,各子系统前端要可以互相跳转,而各子系统只访问自身子系统的后端服务。

    09-57-46.jpg

  1. 各子系统不单独作用户登陆控制,但要求整个系统要有用户登陆控制。好比在web端直接访问某个子系统的界面,须要验证用户是否登陆,若没登陆须要将页面跳转至登陆页面,若已登陆,则将请求转发至对应的子系统的后端服务。

2. 方案设计

2.1 web端的融合方案

  所谓web端的融合,就是要经过同一个域名(也能够是同一个IP地址)去访问不一样的子系统的web端。用户登陆只在其中一个子系统(这个项目是在主系统中)实现,若是使用同一个域名访问不一样的子系统,浏览器会自动携带cookie(跨域的话,浏览器不会自动携带cookie),这样访问每一个子系统时都会携带cookie,后端服务才能利用cookie进一步判断用户是否登陆。
  首先,要能经过同一个域名访问全部子系统web端,就须要全部子系统的web端静态文件都放置在同一个web服务器的下,为了不冲突,这些子系统的静态文件不能都放在web服务器的根目录下,而须要在web服务器的根目录下为每一个子系统都单独新建一个目录来存放各自的静态文件。而后,为了区分来自同一个域名的请求是要访问哪一个子系统,就须要在URL里为不一样的子系统指定不一样的前缀
  下面给出使用nginx作web服务器,web端融合的nginx配置:前端

# 指定静态文件的根目录
root /opt/www;

# 若是访问根目录,直接重定向至主系统
location = / {
        rewrite / /mainsubprefix;
}

# 主系统前缀为 /mainsubprefix
# 主系统静态文件放置在根目录下的 manisubstatic 里,当前配置就是位于 /opt/www/manisubstatic 下
location /mainsubprefix {
        try_files $uri $uri/ /manisubstatic/index.html;
}

# 子系统一前缀为 /suboneprefix
# 子系统一静态文件放置在根目录下的 subonestatic 里,当前配置就是位于 /opt/www/subonestatic 下
location /suboneprefix {
        try_files $uri $uri/ /subonestatic/index.html;
}

# 子系统二前缀为 /subtwoprefix
# 子系统二静态文件放置在根目录下的 subtwostatic 里,当前配置就是位于 /opt/www/subtwostatic 下
location /subtwoprefix {
        try_files $uri $uri/ /subtwostatic/index.html;
}

2.2 后端用户登陆验证方案

2.2.1 方案介绍

2.2.1.1 简单分布式系统权限认证方案

  上面咱们已经肯定了不一样子系统web端融合的方案,各子系统的web端融合后,对外展示就是一个web端了,因此接下来的讨论中都是基于一个web端(内部能够是多个子系统的web端的融合)进行的。
  根据上一节的需求分析,整个系统由多个子系统组合而成,这实际上能够理解为就是很常见的分布式系统,所不一样的就是分布式系统通常只有一个前端界面,并且分布式系统内部的各服务通常都容许互相通讯。但这里咱们能够先用一个简单的分布式系统权限认证方案作入手分析,以下图所示为一个简单分布式系统权限认证方案,提供一个独立的权限认证服务,而后各子系统后端经过调用权限认证服务判断用户是否登陆。nginx

      10-29-06.jpg

  如今来分析一下上面的方案,优缺点以下:web

  • 优势:简单、容易理解,实现方便。
  • 缺点:各子系统后端与认证服务器耦合过紧。若是认证服务器要作调整,须要增长参数,这样每一个子系统就都得修改而且从新测试了,尤为是若是这些子系统是由不一样的公司开发的,势必会提升项目的沟通成本,项目的进展就可想而知的慢了。

2.2.2.2 增长代理层的方案

  为了解决上面的方案中各子系统后端和认证服务器紧耦合的问题,这里在web端和各子系统后端服务之间增长一层代理层,这样全部的请求都会先到代理层,代理层负责验证用户是否登陆,并返回失败(用户未登陆),或转发请求至对应的子系统后端(用户以登陆)。以下图所示。数据库

      13-52-50.jpg

  增长代理层方案优缺点以下:后端

  • 优势:该方案解耦了各子系统后端和认证服务器,并且代理层相对各子系统的web和后端服务是透明的,各子系统研发团队能够不用考虑用户登陆验证的问题,只需保证自身系统的正常运行便可。
  • 缺点:整个系统增长了代理层服务,系统故障风险提高、稳定下降;各子系统的web端在解析每一个请求的响应时,须要处理代理层用户未登陆的响应,并跳转至登陆页。

2.2.2 方案对比

方案 简单分布式权限认证方案 增长代理层方案
优势 简单、已理解、已实现 各子系统后端与认证服务器紧耦合
缺点 对各子系统后端透明,各子系统研发团队只需保证自身系统运行正常 增长了新服务、引入了不稳定性;各子系统的web端在解析每一个请求的响应时,须要处理代理层用户未登陆的响应,并跳转至登陆页

  根据目前项目相关的信息,系统暂没有可扩展性要求,并发量也不大。稳定性方面,前面也提到了简单分布式权限认证方案相对稳定。接下来咱们从业务场景和研发团队两个方面进一步分析两个方案。
  业务场景方面,认证服务器是彻底跟业务紧耦合的,它须要访问业务数据库的用户表;代理层服务器是业务松耦合的,它跟业务惟一的耦合就是须要调用认证服务器去验证用户是否登陆。众所周知,与业务紧耦合的服务确定比与业务松耦合的服务改变几率要大的多。但仅就目前的用户登陆态验证接口,彷佛都是经过cookie去校验,因此无论认证服务器内部怎么验证方案怎么变,只要保证接口不变,其余调用端都不用改变。因此从业务场景方面,两个方案差很少,由于当前方案要解决的用户登陆验证场景几乎不会变化。
  研发团队方面,每一个子系统都是由不一样地区的公司负责,很明显若是每一个公司只负责各子的子系统正常运行是最简单的了。参与过异地多团队项目的人应该都知道,跨地域团队的沟通效率是很是底的,若是须要常常联调,效率就更低了。从这个方面来说,增长代理层方案要胜出一点。跨域

2.2.3 方案选定

  没有最好的方案,只有最合适的方案。咱们结合了项目背景和研发人员的安排,选择了增长代理层的方案。浏览器

2.3 系统总体方案

  通过上面的分析,最终整个系统的方案以下图所示,这里特地将nginx放进来是由于涉及到一个跨域的问题,web端不能直接调用代理层服务器,不然浏览器认为跨域,不会自动携带cookie,因此这里使用nginx作了一层反向代理,先后端都经过一个nginx代理就不存在跨域的问题了。也就是说全部后端服务器都要通过同一个域名访问,那么每一个子系统的后端服务也就须要提供不一样前缀以区分请求服务器

    15-21-26.jpg

  上图中,代理层服务须要能接收指定前缀的请求,而后验证登陆态,并返回失败响应或转发请求至对应后端服务。
  这里给出系统总体方案后端服务的nginx配置(web配置前面已经给出):cookie

# /mainsubbackend 为主系统后端服务API前缀,proxy_pass 后是主系统后端服务地址
location /mainsubbackend {
        proxy_pass http://127.0.0.1:15001;
}

# /sub1backend 为子系统一后端服务API前缀,proxy_pass 后是子系统一后端服务地址
location /sub1backend {
        proxy_pass http://127.0.0.1:15003;
}

# /sub2backend 为子系统二后端服务API前缀,proxy_pass 后是子系统二后端服务地址
location /sub2backend {
        proxy_pass http://127.0.0.1:15003;
}

3. 代理层实现

  代理层服务须要能接收指定前缀的请求,而后验证登陆态,若是用户已登陆,将请求转发至对应后端服务,否正返回失败响应。实现方面没有什么难度,主要是如何实现转发功能,由于我用的是Go语言实现的,因此这里指出Go语言是如何实现转发功能的。Go语言net/http/httputil包里的ReverseProxy自带了转发功能,这里给个简单的例子:

// 用带转发的目标地址建立一个 ReverseProxy 对象指针
proxy := httputil.NewSingleHostReverseProxy(dstAddress)
// 将请求 c.Request 发送至 dstAddress, 响应写会 c.Writer。实际上就是实现了转发
proxy.ServeHTTP(c.Writer, c.Request)

4. 总结

  当把整个系统完成后,再来回顾下系统架构,发现其实代理层就是一个网关,只是代理层只实现了用户登陆验证。这个项目只须要统一的用户登陆校验,因而最开始就把精力放在了如何去校验用户登陆态的微观层,忘了从宏观层面来梳理下整个系统,以致于最开始老是出于混乱中,直到实现了整个系统才看清整个系统的架构实际上是业界成熟通用的架构。固然,还一个很重要的缘由就是对网关的不熟悉。
  因此说,看事物仍是要看到其根本。最后以一个禅语故事结束本文:

无名禅师拿起笔来,在纸上写了一个“我”字,但这个“我”倒是反方向写出来的,就像是印章上的文字。禅师写完后问道:“这是什么?”
小沙弥回答说:“这是一个字,只不过写反了。”
禅师又问:“那你能看出这是个什么字吗?”
小沙弥回答说:“是个‘我’字。”
禅师又说:“那么写反的‘我’字算不算字呢?”
小沙弥说:“写反的字怎么还能算字呢?”
禅师说道:“既然不算,那你为什么会说这是个‘我’呢?”
小沙弥马上改口说:“算。”
禅师又说:“既然算个字,那你为何又说这个字反了呢?”
小沙弥无言以对。
无名禅师微笑着说道:“正写是字,反写也是字,你既然说它是个反写的‘我’字,就说明在你内心是真正识得‘我’的原字的;相反,若是你不知道原来的字是什么模样,就算我写反了,你也无从知晓,只怕当人告诉你这是的‘我’字后,遇到正写的‘我’,你倒要说是反写的了。”
小沙弥如有所悟地点点头,禅师接着说道:“一样的道理,好人是人,坏人也是人,你只需明了人的本性,能一眼辨出善恶,那么度化坏人又有何难呢?”
小沙弥所以顿悟。
其实无名禅师旨在告诉咱们,世间万物皆有其本相,只要明悉本相,那么心中就不会有迷惘疑惑。任其正反,均可不失其道,不乱其规,心如明镜,本相可照。