本文讲述基于 OpenResty  的接口网关设计,主要谈及接口网关的请求路由安全认证IP 与 URI 白名单、加解密与验签名流程等)这两部分内容,其中涉及到的 NginxOpenResty 等相关内容会作简单介绍。


〇、前言

笔者曾参与开发两个接口网关的项目,一个是基于 Tomcat 的应用提供的网关服务,另一个是基于 OpenResty 的 Nginx 应用提供的网关服务。经过两个网关项目的开发,笔者在接口网关开发方面稍微积累了一些经验,故在此把这些经验分享出来一起交流学习。由于基于 OpenResty 的 Nginx 网关普遍被认为是更优的方案,故本文主要针对基于 OpenResty 的 Nginx 网关进行讲述。当然,由于不同的并发数量级,不同的业务场景,接口网关的设计多种多样,本文所述其中较为简单且轻量级的一种。


注:由于笔者经验与知识有限,文章中如有错误或偏颇,欢迎探讨和指正(作业部落提供文章按块批注功能,非常欢迎提批注,笔者会及时修正)。

一、什么是接口网关

1.1 定位

接口网关,顾名思义,是企业 IT 在系统边界上提供给外部访问内部接口服务的统一入口。这里的外部可以指客户端、浏览器或者第三方应用等,在这种情况下,接口网关可以有多种定位:

  • 提供后端服务面向 Web App 或者 Mobile App 的 APIGateway

  • 作为开放平台面向 Partner 的 OpenAPI

在笔者的工作中,同样把面向客户端的网关称作 APIGateway,把作为开放平台提供给第三方服务的网关称作 OpenApi。本文主要以 OpenApi 作为接口网关为例来讲述。


1.2 功能

作为企业 IT 系统的统一入口,接口网关可提供请求路由与组合协议转换安全认证服务鉴权流量控制日志监控等服务。在笔者的工作中,主要在接口网关上实现了请求路由安全认证的功能,题目中所说的“设计”,主要是指请求路由与安全认证方面,暂不涉及流量控制或日志监控等其他方面的设计。


二、为什么需要接口网关

正如上文所言,网关接口为企业应用提供了丰富的功能,而笔者在工作中开发的接口网关主要提供请求路由与安全认证的功能,那么在回答“为什么需要接口网关”的时候,需要对这两者多加阐述。

2.1 请求路由

企业提供内外两网,在没有接口网关时,提供外部服务的应用需要部署在外网。随着服务的增多,部署在外网的应用越来越多,在服务的安全压力与维护成本增大的情况下,需要一个统一的接口网关“隔离”内外服务。企业提供的服务(无论内部服务还是外部服务)均部署在内网,而由部署在外网的网关接受请求,并路由到内网服务。在这种情况下,既有利于对外屏蔽企业内部服务部署细节,提供统一的服务访问地址,又便于管理与维护内外部服务接口,便于演进与重构服务。这是接口网关提供请求路由的作用。

2.2 安全认证

在没有接口网关时,企业对外服务直接由外部访问,身份验证与数据加解密等工作都需要每一个对外服务本身去处理增加了服务本不该有的职责,并且增加了服务开发的难度与工作量。实际在大多数情况下,可以将身份验证与数据加解密等安全工作可以从服务抽离,统一由接口网关负责处理。接口网关作为入口,对外验证调用方的 IP,身份以及接口访问权限等,并且可以解密数据后再将请求路由到服务。这是接口网关提供安全认证的功能。

以上是实际工作中涉及的为什么需要接口网关的其中两个原因,当然原因远不止此,有兴趣的读者可以阅读其他文章,比如 《谈API网关的背景、架构以及落地方案》 或者 《微服务:从设计到部署》(英文原文:Microservices: From Design to Deployment)。接下来的章节我们开始探讨如何开发接口网关。


三、如何开发接口网关

我们先看看工作中设计的提供请求路由安全认证功能的接口网关的架构。

不过在介绍接口网关的设计之前,我们先来了解一下关于 Nginx 与 OpenResty 的基础知识。

3.1 Nginx 与 OpenResty 简介

3.1.1 Nginx 简介

Nginx 是世界第二大 Web 服务器,仅次于 Apache,然而由于其极高的性能可处理海量的互联网请求,现在已经成为业界高性能 Web 服务器的代名词


它的主要特征是高性能、高扩展性、高可靠性、低内存消耗、单机支持 10 万以上的并发连接,支持热部署,以及使用最自由的 BSD 许可协议。其中,Nginx 可以处理高并发压力下的并发请求的原因如下:

  • 事件驱动模型设计

  • 全异步网络 I/O 处理机制

  • 极少的进程切换

  • 内存消耗低极度“压榨”服务器硬件资源

除了基于事件驱动的架构使其支持百万级的 TCP 连接,另外高度模块化的设计自由的许可证使其拥有非常多扩展其功能的第三方模块,也是它的重要特性。所以,后来才会有 OpenResty 的诞生。


我们看一个 Nginx 作简单配置来提供服务的例子:

worker_processes 1; events {  worker_connections 1024; } http {    upstream backend {        server 127.0.0.1:8080    }    server {        location /back {            proxy_pass http://backend;        }    } }

上述配置文件中,分别在 event、http、server 以及 location 块配置项中做了一些简单的配置,当安装完并启动 Nginx 后(监听 80 端口),访问到 /back 路径下的请求会被转发到本地 127.0.0.1:8080 服务上。


3.1.1 OpenResty 简介

根据官网定义,OpenResty 是一个通过 Lua 扩展 Nginx 实现的可伸缩的 Web 平台。其核心是基于 Nginx 一个 C 模块将 Lua 语言嵌入到 Nginx 服务器中,对外提供一套完整的 Lua Web 的 API,并透明支持非阻塞 I/O,提供协程 —— “轻量级线程”、定时器等,从而极大地降低了高性能服务端的开发难度和开发周期


OpenResty 将两个极为优秀的组件 Nginx 与 Lua 进行糅合,一方面保留了 Nginx 高性能 web 服务特征,另一方面有提供 Lua 特性在极少损失性能情况下便于业务功能的开发。根据官网介绍,OpenResty 非常便于用来搭建能够处理超高并发、扩展性极高的动态 Web 应用Web 服务动态网关


我们也是因为 OpenResty 的这些特性,特别是它对搭建动态网关的友好支持,才选择了基于 OpenResty 来开发我们的接口网关 —— APIGateway 与 OpenApi。


开发接口网关使用到的 OpenResty 一个重要知识:OpenResty 对于一个请求的处理流程。Nginx 把一个请求分为不同的阶段,从而让第三方模块通过挂载行为在不同的阶段来定制自己的行为;OpenResty 拥有同样的特性,不过在不同阶段挂载的是 Lua 脚本。下图是基于《OpenResty 最佳实践》原图重绘而来:


从上图可知,OpenResty 处理请求大致分为四个阶段:

  • 初始化阶段(Initialization Phase)

  • 重写与访问阶段(Rewrite / Access Phase)

  • 内容生成阶段(Content Phase)

  • 日志记录阶段(Log Phase)

我们看一个 OpenResty 作简单配置来提供服务的例子:

worker_processes 1; events {  worker_connections 1024; } http {  resolver 127.0.0.1;  lua_package_path '$prefix/lua/?.lua;;';  init_by_lua_block {    # ...  }  init_worker_by_lua_file lua/init_work_by_lua.lua;  server {    listen 80;    location / {      rewrite_by_lua_file lua/rewrite_by_lua.lua;      access_by_lua_file lua/access_by_lua.lua;      proxy_pass http://<url>;    }  } }

上述配置文件中,分别在 event、http、server 以及 location 块配置项中做了一些简单的配置,当安装完并启动 Nginx 后(监听 80 端口),首先执行 init_by_lua_block、init_worker_by_lua_file 进行初始化,接着接受请求,所有的请求都会匹配上 "/" 路径,进而执行 rewrite_by_lua_file、access_by_lua_file 进行重写与访问,最后转发请求到本地 127.0.0.1 服务上。


在实际的接口网关开发中,我们主要是使用到了 OpenResty 中初始化阶段的 init_by_lua*、init_worker_by_lua*、重写与访问阶段 的 rewrite_by_lua*、access_by_lua* 以及内容生成阶段 content_by_lua* 过程。


3.2 接口网关的架构

这一节是本文的核心内容,重点讲述接口网关的架构设计。如前文所述,本文主要以 OpenApi为例来讲述接口网关的架构设计。先看图:

下面我们来一步步来分析架构图的各个部分,首先是两层的 HAProxy 。


3.2.1 两层 HAProxy 代理

根据维基百科定义,HAProxy 是一个使用 C 语言编写的自由及开放源代码软件,其提供高可用性负载均衡,以及基于 TCP 和 HTTP 的应用程序代理

如图所示,隔离的内网与外网上分别提供了 HAProxy 代理, 外层暂且称为 HAProxy internet ,内层称为 HAProxy internal。外层暴露于外网中,使用统一地址如 http://openapi.company.com 来接受外部请求(这里指第三方的请求);中间是基于 OpenResty 的 Nginx 网关层,外部请求经过网关后通过 HAProxy internal 转发到内网的服务上,内网服务遵循 Restful 风格,网关转发到内网的地址由接口网关控制。


然而,目前的代理架构受到了当前整体架构的约束,实际上两层的 HAProxy 代理并不是必需的

  • 对于外层  HAProxy internet,由于我们使用了与 HAProxy 紧密结合的 Openshift 架构,所以多了一层 HAProxy 的转发;一般情况下,基于 OpenResty 的 Nginx 网关层可以直接在外网上提供服务。

  • 对于内层的 HAProxy internal,由于我们当前还没有实现服务治理,所以需要内层的 HAProxy internal 进行一层转发;当实现了服务治理,可以消除内层 HAProxy 代理,减少转发消耗。


在我们当前的系统量级下,这两层 HAProxy 转发消耗非常小可以被接受,所以调整架构的优先级还不高,以后再慢慢演进。


3.2.2 接口网关

接下来这一节是最为重点的接口网关的设计。接口网关主要利用前文所述的 OpenResty 执行阶段请求与响应进行流程处理,包括接口地址的重写IP 与资源白名单的控制请求的解密与验签请求的路由以及响应的签名与加密等。

这里分成主流程配置服务安全服务三部分进行讲述。


3.2.2.1 主流程设计

主流程是网关的核心,是请求处理的控制中心;它是通过 OpenResty 的 Lua 脚本处理流程来实现对请求的处理。

A. 主流程

  1. 在 OpenResty 服务启动之后,首先通过 init_by_lua_block 阶段初始化常量(包括调用配置服务以及安全服务所需的主机地址、端口、URL 地址等)、引入依赖(包括常用的 http 以及 cjson 依赖等)等作为全局使用;

  2. 接着通过 init_worker_by_lua_file 阶段设置定时任务调用内网配置服务来缓存配置,为处理第三方的请求做准备,其中加载的配置可供 URL 重写(即接口映射)、IP 以及资源(URI)白名单限制、请求的解密验签以及响应的签名加密使用,详情查看配置服务一节。

  3. 当第三方请求通过 HAProxy Internet 进入到网关后,根据配置通过 rewrite_by_lua_file 阶段做 URL 重写(即接口映射)。

    • 服务接口 URL 发生变更,为了兼容旧的第三方调用,需要重写第三方请求 URL 到新服务接口上

    • Restful 接口的 Path Variable 在 Nginx 环境与在 Tomcat 环境上正则匹配的差异

    • 需要重写的原因可能有:

  4. URL 重写后,通过 rewrite_by_lua_file 进入访问控制阶段,此时根据授权的第三方 IP 白名单列表,授权予第三方的开放接口列表,校验请求的 IP 以及 URL。

  5. IP 与 URI 校验通过后,同样在 rewrite_by_lua_file 阶段根据配置调用内网的安全服务进行请求的解密与验签,获取明文。

  6. 在 content_by_lua_file 阶段通过 ngx.location.capture 将原请求头部信息以及参数等信息封装到子请求中,借助自请求转发原请求到开发接口服务中。

    • 注意:根据官方文档说明,ngx.location.capture 发送子请求会缓存响应在内存中,直到整个请求处理结束。那么,当有响应报文特别长或者请求并发非常高时,需要使用 cosocket 来替代 ngx.location.capture,避免因内存不足造成网关服务失效。

  7. 同样在 content_by_lua_file 阶段根据配置调用安全服务进行响应的签名与加密,获取签名与密文返回给第三方。


B. 文件结构

项目的大致结构如下,主要分为 Lua 代码目录和环境配置目录。

--openapi  --lua    --access_by_lua.lua    --cache_management.lua    --content_by_lua.lua    --init_work_by_lua.lua    --rewrite_by_lua.lua    --security.lua  --prod    --Dockerfile    --nginx.conf  --sit    --Dockerfile    --nginx.conf  --README.md

C. 主流程在 conf 中的配置

# Nginx worker 进程个数,直接影响性能。 # 如果确认不会出现阻塞式调用,那么有多少 CPU 内核设置多少个进程 # 如果有可能出现阻塞式调用,需要配置多一些进程 worker_processes 1; events {  worker_connections 1024; } http {  # 内网地址  resolver xxx.x.x.xxx yyy.y.y.yyy;  # 日志格式配置  log_format graylog2_format  '$remote_addr - $remote_user [$time_local] "$request" '                              '$status $body_bytes_sent "$http_referer" '                              '"$http_user_agent" "$http_x_forwarded_for" '                              '<msec=$msec|connection=$connection|connection_requests=$connection_requests|millis=$request_time>';  # 日志路径配置  access_log syslog:server=<host>:<port> graylog2_format;  error_log  syslog:server=<host>:<port> warn;  # 配置 Lua 包地址  lua_package_path '$prefix/lua/?.lua;;';  init_by_lua_block {    # 引入依赖(可能会污染全局环境,待研究)    http = require "resty.http"    cjson = require "cjson"    cache_management = require "cache_management"    ...  }  # 设置定时任务缓存配置,及上面的 cache_management 模块  init_worker_by_lua_file lua/init_work_by_lua.lua;  # Nginx Web 服务配置  server {    listen 80;    # ngx.location.capture 子请求代理,转发原请求到接口服务    location = /ngx_proxy/ {      internal;      proxy_set_header Accept-Encoding '';      proxy_pass http://$context$http_host_suffix$proxy_uri;    }    # 匹配所有请求,进行 URL 重写、访问控制、转发请求以及响应处理(各阶段的处理在此配置)。    location / {      set $context '';      ...      rewrite_by_lua_file lua/rewrite_by_lua.lua;      access_by_lua_file lua/access_by_lua.lua;      content_by_lua_file lua/content_by_lua.lua;    }  } }

D. URL 规范

内网服务遵循的 URL 格式为 http://<host>:<port>/<context>/path/to/your/api,应用上下文根紧跟在 <host>:<port> 之后,以便统一获取来找到配置。比如:http://172.0.8.177:8080/user/users/{uid}/info,其中 user 为应用上下文根,紧跟在 172.0.8.177:8080 之后。


E. 样例

内网用户信息服务由原来的 API:/user/users/{uid}/info 提供,后来迁移至 API:/user/users/{uid}/user-info,当第三方 CampA (IP 为 172.0.1.172) 发起 GET 请求时,请求 URL 为 http://openapi.company.com/user/users/27/info?thirdparty=CampA&cp=fj375x...sign=abxuos8nb...。

  1. 初始化常量和依赖等

  2. 通过 CampA 与 user Context 获取第三方配置

  3. HAProxy Internet 接收请求发到 OpenApi 接口网关,OpenApi 把 /user/users/27/info URI 重写为 /user/users/27/user-info/

  4. 校验第三方请求 IP,在 IP 白名单中,校验通过;校验 URI /user/users/27/user-info在授权的 URI 中,校验通过

  5. 调用安全服务对请求进行解密与验签,解密成功,验签通过,获取明文

  6. 将拥有明文的请求转发到开放接口服务

  7. 获取响应,调用安全服务对响应报文进行签名与加密,返回给第三方 CampA。

3.2.2.2 配置服务设计


A. 数据库表设计

  • openapi_thirdparty_config

  • openapi_api_config

  • openapi_api_mapping

B. 配置服务接口响应

{    # 接口映射配置    "apiMapping":{        "$context":{            "$fromApi":"$toApi"        }    },    # 接口白名单配置、加解密配置    "apiConfig":{        "$channel $context":{            "$httpMethod $uri":{                "reqNeedDecrypt":false,                "respNeedEncrypt":false            }        }    },    # IP 白名单配置,验签名配置    "channelConfig":{        "$channel":{            ips:{                "$ip":1            },            "reqNeedVerifySign":false,            "respNeedSign":false,            "needCheckIp":false        }    } }

3.2.2.3 安全服务设计

为了保证请求或响应的完整性、以及请求或响应来源的合法性,双方传输需要进行签名;另外,由于可能开放接口的请求或响应会包含敏感信息,需要进行加密传输。这里的安全服务就是指请求的解密与验签和响应的签名与加密服务。

A. 算法约定

  • 对称加密算法:3DES(DESede/ECB/PKCS5Padding)

  • 非对称加密算法:RSA(RSA/ECB/PKCS1Padding)

  • 签名算法:SHA1WithRSA

B. 公钥约定

双方预先交换 RSA 公钥

  • 双方公钥编码方式:UTF-8 编码的 Base64String

  • 双方进行加解密与验签名可使用同一把 RSA 公私钥或者分别使用各自的公私钥,双方约定即可

C. 第三方请求流程示意

其中,添加统一参数为必选步骤请求签名、请求加密、响应解密、以及响应验签都是可选步骤

  • 无论是 GET、POST 或者其他方式的请求,第三方在访问平台开放接口前,都需要添加统一参数到 request parameter 中

    • 统一参数包括第三方应用名、请求时间戳、随机不重复字符串 nonce 等

  • 验签名属于应用维度 —— 针对应用做验签名(比如:按照约定需要对第三方应用 A 进行验签,则应用 A 访问数禾任何接口都需要签名)

  • 加解密属于接口维度 —— 针对接口做加解密(比如:同一个第三方访问 A 接口需要加密,而访问 B 接口可以不需加密)

D. 加解密示意(以第三方请求为例)

E. 验签名示意(以第三方请求为例)

3.2.3 架构总结

由于 Nginx 与 Lua 本身杰出的性能,在当前的系统量级与整体 IT 架构下,我们使用这样的接口网关架构已经可以支撑较大的并发请求。在最后的这一节,我们不妨回顾一下前文讲述的接口网关架构,看看目前性能上仍存在着的两个主要待改进的地方。


  1. 两层 HAProxy 代理:在使用更优产品替代 Openshift 架构的情况下,直接部署接口网关到公网,可消除外层 HAProxy 代理;在实现服务治理的情况下,由接口网关直接转发请求到服务,可消除内层 HAProxy 代理。

  2. 安全服务性能:加解密验签名等安全服务是以内部服务的方式提供给接口网关,而且使用了性能不太好的 ngx.location.capture 转发原请求,在系统量级增大后会遇到性能瓶颈,可通过使用高性能的 Lua 脚本在接口网关层提供安全服务,从而提升安全服务性能。


除了以上主要的两点,随着系统量级的提升与整体 IT 架构的演进,接口网关的架构也会随之调整和演进,在各个方面都尽可能地优化性能,以适应更大系统量级的需求。



参考书籍:

1.《深入理解 Nginx(第2版)》
2.《Java 加密与解密的艺术(第2版)》

参考文章:

1. 谈 API 网关的背景、架构以及落地方案
2. 微服务:从设计到部署 (Microservices: From Design to Deployment)
3. 锤子手机发布会提到的 OpenResty 是什么?
4. OpenResty 最佳实践


本文作者:一帅同学   

文章来源:数禾科技技术团队公众号【泛金融技术】,欢迎关注、投稿、交流。

转载声明:未经授权不得转载,授权后转载请注明出处并附上原文链接。