网关——不同团队子系统登录验证问题总结

1. 需求分析

1.1 背景介绍

  一个主系统和若干其他团队(或公司)开发的子系统(主系统和每个子系统都有前后端)需要对外展现为一个系统(实际上没有主系统和子系统之分,这里是因为主系统研发团队需要对整个系统负责,而子系统研发团队只对自身系统负责)。因时间关系,主系统研发团队不再重新开发界面,而是直接跳转到子系统界面。这样的话,就相当于一个系统有多个界面和多个后端。一般校验用户是否登录都是在各自的后端完成的,而多个子系统的话就会有多个账号体系,这对于系统融合就很麻烦了。这里为了简单,要求各子系统本身不做用户登录态校验,统一由主系统研发团队负责用户登录态校验工作。

1.2 需求具体化

  1. 如下图所示,我们由一个主系统和两个子系统,这里需要将这三个系统对外展现为一个系统。

    

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

    

  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端的融合)进行的。
  根据上一节的需求分析,整个系统由多个子系统组合而成,这实际上可以理解为就是很常见的分布式系统,所不同的就是分布式系统一般只有一个前端界面,而且分布式系统内部的各服务一般都允许互相通信。但这里我们可以先用一个简单的分布式系统权限认证方案做入手分析,如下图所示为一个简单分布式系统权限认证方案,提供一个独立的权限认证服务,然后各子系统后端通过调用权限认证服务判断用户是否登录。

      

  现在来分析一下上面的方案,优缺点如下:

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

2.2.2.2 增加代理层的方案

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

      

  增加代理层方案优缺点如下:

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

2.2.2 方案对比

方案 简单分布式权限认证方案 增加代理层方案
优点 简单、已理解、已实现 各子系统后端与认证服务器紧耦合
缺点 对各子系统后端透明,各子系统研发团队只需保证自身系统运行正常 增加了新服务、引入了不稳定性;各子系统的web端在解析每个请求的响应时,需要处理代理层用户未登录的响应,并跳转至登录页
  根据目前项目相关的信息,系统暂没有可扩展性要求,并发量也不大。稳定性方面,前面也提到了简单分布式权限认证方案相对稳定。接下来我们从业务场景和研发团队两个方面进一步分析两个方案。
  业务场景方面,认证服务器是完全跟业务紧耦合的,它需要访问业务数据库的用户表;代理层服务器是业务松耦合的,它跟业务唯一的耦合就是需要调用认证服务器去验证用户是否登录。众所周知,与业务紧耦合的服务肯定比与业务松耦合的服务改变概率要大的多。但仅就目前的用户登录态验证接口,似乎都是通过cookie去校验,所以不管认证服务器内部怎么验证方案怎么变,只要保证接口不变,其他调用端都不用改变。所以从业务场景方面,两个方案差不多,因为当前方案要解决的用户登录验证场景几乎不会变化。
  研发团队方面,每个子系统都是由不同地区的公司负责,很明显如果每个公司只负责各子的子系统正常运行是最简单的了。参与过异地多团队项目的人应该都知道,跨地域团队的沟通效率是非常底的,如果需要经常联调,效率就更低了。从这个方面来讲,增加代理层方案要胜出一点。

2.2.3 方案选定

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

2.3 系统整体方案

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

    

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

# /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. 总结

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

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