面向侧面的程序设计(aspect-oriented programming,AOP),经过将解决特定领域问题的代码从业务逻辑中独立出来,从而提升代码的可维护性。html
从主关注点中分离出横切关注点是面向侧面的程序设计的核心概念。分离关注点使得解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中再也不含有针对特定领域问题代码的调用,业务逻辑同特定领域问题的关系经过侧面来封装、维护,这样本来分散在在整个应用程序中的变更就能够很好的管理起来。 - 维基百科nginx
示例是根据最近正在负责的 APP 后端项目简化版,需求简单说以下:git
APP 端会对全部请求进行加密,服务器端要对加密结果进行校验,确保正确以及未篡改;github
经过手机号来登陆,采用基本的 token 机制验证登陆;web
有企业、小组以及员工的层级关系,后期必须考虑根据公司来分表/集群;redis
提供涉及到权限的 REST 风格的接口(某种程度上相似 Postgrest,可是进行了拓展,后面会有专门文章介绍)sql
思路:首先整个目前项目的主关注点 (core concern) 是 REST 风格的资源服务器 —— 即经过约定俗成的风格来对应具体的数据/资源操做。在这个功能外,须要完成的其余关注点包括:数据库
全部请求加密校验json
登陆验证后端
资源的权限管理以及获取
因而,在 Sinatra 中,能够经过 extensions 的方式将请求加密校验完成,配合 before 来进行统一处理:
require 'sinatra/base' module Sinatra module RequestHeadersVerify module Helpers def headers_valid? # 此处省略真实业务代码 false end end def self.registered(app) app.helpers RequestHeadersVerify::Helpers app.before do unless headers_valid? halt 400, json(ResponseErrror::InvalidHeadersError.new) end end end end register RequestHeadersVerify end
最终采用"中间件"的方式,在请求的最前面一层(横切关注点 crosscutting concerns)将非法请求进行拦截。
因而紧接着第二个流程,验证用户是否登陆,与获取当前联系人所在的公司、小组、以及其管理的小组信息同样,这里最快速/方便的作法就是经过 helpers 来实现:
require 'sinatra/base' module Sinatra module UserSessionHelpers HTTP_USER_TOKEN_KEY = 'HTTP_AUTH_TOKEN' def current_user @current_user ||= ( user = User.first token: env[HTTP_USER_TOKEN_KEY] halt 400, json(ResponseErrror::InvalidTokenError.new) unless user user ) end end module OrganizationHelpers # 这里省略掉相关 helpers 代码 end helpers UserSessionHelpers helpers OrganizationHelpers end
最终在 REST 相关的构建代码中,就不须要去考虑用户请求加密的内容,也不须要去考虑用户是否登陆(由于若是须要使用到用户信息可是用户没有登陆,会直接抛出错误返回)。只须要按照约定的设计风格,把请求的内容在校验了内容和权限后,转成对应的数据库操做,最终再按照约定的内容返回。
初版已经尽量的考虑到 解决特定领域问题的代码从业务逻辑中独立出来。可是现实开发里面常常会涉及到多人开发、跨语言合做、更快速的迭代等等的问题,最终须要把他们拆成独立的低耦合度的 Server。因而随之而来的是如何在服务间进行通信/共享数据。
这里的方案选择一般会根据实际业务以及难易程度来权衡,例如最快捷的 webServer 的方式内部通讯,稍微复杂点的基于 TCP 的 RPC 通讯方案(例如 thrift),或者某些特殊的情景,例如是生产者/消费者关系的话,则可能经过 MQ 来进行通讯。最终咱们采用的是经过 Nginx 的 lua 模块来将 server 以面向侧面的思路耦合。
首先,Nginx 的 Lua 模块能够作什么?若是能够,单纯 nginx 和 lua 就能够完成完整的 web 服务。能够链接 redis、memcache、postgresql 等等,同时能够取得请求的全部内容,能够设置返回的头部、正文。配合 lua 的对数据处理能力,基本功能均可以实现。同时 nginx 的 lua 模块总体都是异步,因此性能也相对较好。固然也能够经过 lua 脚原本控制权限,若是验证经过则继续下面的操做,例如是 proxy_pass 代理,简单的示例以下文:
location = /foo { access_by_lua_block { -- check the client IP address is in our black list if ngx.var.remote_addr == "132.5.72.3" then ngx.exit(ngx.HTTP_FORBIDDEN) end -- check if the URI contains bad words if ngx.var.uri and string.match(ngx.var.request_body, "evil") then return ngx.redirect("/terms_of_use.html") end -- tests passed } proxy_pass http://blah.blah.com; }
不过,咱们这里将用到的主要仍是 proxy_pass
, 和 ngx.location.capture
,基本代码以下:
<!-- 禁止任何如下划线开始的请求地址 --> if string.sub(ngx.var.uri, 2, 2) == "_" then ngx.exit(404) end local cjson = require "cjson" local custom_header_prefix = "V-" local request_args = ngx.req.get_uri_args(64) local request_body = ngx.req.read_body() local request_path = ngx.var.uri local request_method = ngx["HTTP_"..ngx.req.get_method()] for header, _ in pairs(ngx.req.get_headers()) do if string.upper(string.sub(header, 1, 2)) == crm_header_prefix then ngx.req.clear_header(header); end end function res_with_json(body, status) ngx.header["Content-Type"] = "application/json" ngx.print(body) return ngx.exit(status) end function request_to_server(uri) <!-- 发起请求至其余地址并取得结果 --> res = ngx.location.capture(uri..request_path, { body = request_body, args = request_args, method = request_method, }) local json_response = cjson.decode(res.body) <!-- 解析返回内容 --> if not json_response.next == true then res_with_json(res.body, res.status) end for key, value in json_response.params do ngx.req.set_header(custom_header_prefix..string.upper(key), value) end return false end
上面的代码主完成了清理用户恶意提交的请求头,以及 request_to_server
的代码,实现了将原请求内容转发给另外一个接口并得到请求后的内容。获得请求结果后,验证请求的参数。
同时在 nginx 里面经过 stream 和 proxy_pass 的方式来配置多个内部地址:
upstream authentication-server { server 192.168.21.1:6011; server 192.168.21.2:6011; } server { location /_authentication { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; rewrite ^/_authentication/(.*) /$1 break; proxy_pass http://authentication-server; } }
因而,将两个结合起来,就能够实现经过 lua 脚本把原请求的全部参数,包括头部、正文、请求地址、请求方法都带过去,请求另外一个地址(和 proxy_pass 相似),而且能够获得最终返回的结果处理。
拥有这个能力后,即是本文的重点了:在 Sinatra
的初版本中,最终都是 ruby 代码不断调用方法,来完成整个请求的流程。那若是咱们把整个流程的打通交给 Nginx 的话该如何实现呢?
当一个请求进入后,经过 request_to_server
的能力,把请求依次转发给负责 横切关注点 的服务,例如用户请求校验以及登陆校验服务、用户的组织架构服务,最终再去调用主关注点,即本文中的资源服务器;
每次请求完后,根据前一个流程的返回值决定是否进入下一个流程,例如示例中的 lua 脚本是经过返回的 json 里面的 next 参数来决定是否继续往下走。若是没有这个参数则直接返回当前服务的返回值再也不继续请求下去;
若是出现了 next: true
这个关键字,则将返回值中的其余内容以请求头的形式传递给下一个服务,且每一个服务都会彻底信赖这些请求头(因此请求刚进来的时候须要作一些请求头和请求地址处理)。
若是到这里都没有太大问题,你应该能够理解个人意图了。即 Nginx 经过 Lua 脚原本依次请求 横切关注点服务器,若是一路顺畅(每次都有 next: true
),最终会把携带有 横切关注点服务返回的内容的 headers 带给主关注点服务。
因而,在本需求里面,为了保证可拓展性和低耦合性,最终分为了三个服务:
负责请求加密鉴权,用户登陆、密码修改的用户验证服务
负责管理企业结构、获取用户权限的组织架构服务
负责具体的 REST 请求处理的资源服务
当一个用户登陆的请求过来,由于密码错误或者加密鉴权失败,会在用户验证服务就直接返回错误。由于返回内容中没有 next: true
字段,因此直接返回结果;
当一个用户发起了一个发送短信验证码的服务,这个只是用户验证服务的职能,没有必要继续向下走,因而返回了一个没有 next: true
字段的返回值,因而 Nginx 直接返回结果;
当用户登陆的时候,虽然经过了用户验证服务的校验,可是该服务没法获取更多的用户信息,因而把该请求继续传递到组织架构服务,组织架构服务在请求头中拿到了手机号信息,因而直接返回了该手机号所对应的详细信息;
如今发起了一个资源操做的请求,由于用户验证服务没法识别,因此只返回了手机号给 nginx,nginx 继续请求组织架构服务,由于组织架构服务也不能处理,因此继续返回了详细的我的信息给 nginx,nginx 最终拿到这些信息,都经过头部请求了资源服务,而后由于这里是主关注点,也是流程里面最后一个节点,因此经过 proxy_pass
给了资源服务。
最终,这样作的优点:
利用 Nginx 异步的优点来弥补 ruby 服务先天性 IO 处理的不足;
目前只实现了第一条线,即从用户验证 -> 组织信息 -> 资源服务器的顺序,后面若是有须要,能够随时实现其余顺序,而只须要按照在请求头里面加上相应的参数便可,减低耦合性;
三个模块都有各自的业务和特色,能够针对模块去设计缓存方案,并且能够分模块去设计集群方案;
对于开发者而言,更容易完成单个服务的测试用例,而不须要过多在开发过程当中关注联调。