王正华
关键词:数据查询;服务接口;元数据;数据库;系统安全
0 引言
人保财险江苏省分公司2022年保费收入近500 亿元,是整个财产保险行业最大的一家省级分支机构。为支持公司业务发展,江苏省分公司开发了众多适合公司管理需求的应用系统。近年来,为顺应微服务架构和中台战略的潮流,公司积极进行应用的改造,基于微服务将原来的单体应用架构升级为分布式架构,同时为提高安全性,将跨应用的数据库直接访问改造成服务接口调用。在进行应用间调用关系的梳理后,发现大部分调用为数据查询调用,如果每个查询服务都进行独立开发,将耗费大量的资源,因此决定开发一套基于元数据的通用查询接口系统,通过界面进行查询配置,快速生成查询服务接口,加快应用改造的进度,也为后续应用开发打下坚实的基础。
1 系统概述
系统采用Java 作为开发语言,基于传统的三层B/S架构,Spring Boot构建后端应用,前端通过HTML+CSS+JavaScript(jQuery) 提供WEB配置和管理界面。
系统支持单机部署或集群部署,单机部署时使用Ehcache缓存,集群部署时使用Redis缓存,通过F5或Nginx进行负载均衡。
系统主要由元数据配置、数据源动态管理、数据查询接口、接口安全控制、日志监控和配置元数据库及数据缓存等部分构成,其中数据源动态管理、数据查询接口和接口安全控制是整个系统的核心。
总体架构如图1所示。
2 系统设计
2.1 数据源动态管理
系统提供配置界面配置数据源名称、数据库连接的驱动、连接串、用户名和密码以及连接池的大小等参数。系统采用Druid连接池管理数据库连接,开发连接池管理器,在内存中建立数据源名称与连接池之间的映射关系,对连接池进行命名管理。连接池按需创建,系统仅创建实际查询需要的连接池。
注册连接池时,连接池管理器创建DruidData?Source连接池对象,并添加到名称与连接池映射表。
删除连接池时,连接池管理器从映射表中摘除名称对应的连接池,并将连接池添加到待删除连接池队列,由连接池关闭线程在连接池无连接时执行关闭操作并删除连接池。
连接池的连接数等参数调整时,连接池管理器从映射表取得对应的连接池,修改对应的连接池参数。
系统查询数据时,连接池管理器从映射表取得对应的连接池,并通过连接池取得数据库连接,如数据库连接池尚未创建,则创建连接池。
集群部署时,系统启用连接池发现线程定期从元数据表中查询连接池的配置,如发现配置变更则参照上述操作调整连接池。
2.2 数据查询接口
数据查询接口是一个遵循REST 架构风格的API[1],支持根據配置的元数据从关系数据库中查询满足条件的数据,并以适当的格式返回调用者。
2.2.1 元数据配置
系统提供配置界面配置数据查询接口元数据,数据查询接口配置项目包括:
1) 查询的数据表,如果查询多个数据表,定义表之间的连接类型(内连接、左右外连接等)以及连接条件,系统根据数据库元数据可自动生成连接条件。
2) 查询的项目,支持原始的数据表字段、聚合函数,以及基于上述查询项目的表达式运算。
3) 查询的条件,分为固有条件和客户端请求条件两类。客户端请求条件定义客户端请求参数对应的数据表字段或聚合函数,以及实施的控制类型。控制类型包括是否允许模糊匹配、是否为必选项(单个项目必选或一组项目中至少一个必选)。接口查询元数据配置如图2所示。
2.2.2 关键技术
作为通用的数据查询接口,支持常见的数据库类型是系统的基本要求。这方面,系统重点解决以下三个问题:
1) 获取数据库表结构以及数据表之间的关系,用于数据查询接口元数据的配置、查询条件的解析以及查询结果输出格式的选择。然而不同的数据库系统,其数据字典存储的位置也不尽相同,比如Informix使用systables和syscolumns存储数据表和字段的定义,而PostgreSQL使用pg_class和pg_attribute存储数据表和字段的定义。
2) 在涉及大量数据查询时,需要使用合适的参数进行分页处理,但是各种数据库在分页处理上也千差万别,如Informix数据库(v11以上版本)使用“SELECTSKIP m FRIST n …”查询第m条起共n条数据,而Post?greSQL 数据库使用“SELECT … OFFSET m LIMIT n”实现Informix类似功能。
3) 不同数据库系统对GROUP BY和ORDER BY 格式要求不同,如Informix可以使用数字序号代替查询字段,而Oracle要求使用查询字段名称;Informix排序字段可以不出现在查询结果中,而PostgreSQL要求排序字段必须是查询字段。
本系统从两个层面解决上述问题:
1) 通过JDBC的DatabaseMetaData获取数据库共性元数据[2]。
JDBC中,描述数据库表等数据字典的数据称为元数据。系统通过数据库连接获得DatabaseMetaData 对象,并使用该对象查询以下内容:①数据库中包含的数据表、同义词或视图(以下如无特殊说明将数据表、同义词及视图统称为数据表)的Catalog,Schema及数据表名。
②数据表包含的字段以及字段的类型、长度和精度。
③数据表(不包括同义词和视图)主键字段、表之间的主外键关联关系。
2) 通过方言接口实现数据库的差异化处理。
系统定义数据库方言接口,不同的数据库实现差异化的实现类。接口定义了以下方法:
①getSupportFeatures()
取得数据库支持的功能特性,主要用于检查数据库是否支持指定查询起始记录序号、GROUP BY 和ORDER BY的表示方式和限制条件等。
②splitTableName(String name)
将数据表名拆分为Catalog、Schema和Table名称三个部分,以便以合适的参数调用DatabaseMetaData 中的方法获取数据表元数据。
③addLimit(String sql, int start, int count)
在SQL语句中增加查询范围限制条件。
数据库方言工厂通过数据库连接的Databas?eMetaData获得数据库名称及版本,创建适配的数据库方言对象。
系统实现了Oracle、SQL Server、DB2、Informix、MySQL、PostgreSQL、Greenplum 和SQLite 等数据库的方言,同时提供了支持其他数据库的扩展能力。
2.3 接口安全控制
接口安全控制模块对查询请求进行客户端身份认证、防篡改、超时访问、地址白名单、流量管控和查询项目鉴权等安全检查,并保存访问日志记录[3]。
系统使用过滤器实现接口的安全控制,过滤器前置于数据查询接口,只有过滤器检查通过的请求才能到达实际的数据查询接口。
2.3.1 元数据配置
元数据配置功能提供调用者接入代码、签名密钥、白名单地址、允许访问的接口代码、访问流量限制等配置项目,实现上述安全控制功能。
系统定义安全检查接口,签名验证、地址白名单检查、请求超时检查以及流量管控等功能实现该接口,安全检查时,系统根据安全规则中配置的检查类型使用工厂方法创建安全检查对象对请求进行检查,只有通过全部安全检查的请求才予以放行。
2.3.2 关键技术
1) 接口参数的签名
接口参数签名是接口安全的重要组成部分,通过数据签名,可以进行用户身份识别以及参数的防篡改,配合时间戳完成超时访问的控制,避免签名密钥的暴力破解。
系统为每个接口的调用方分配一个接入代码和签名密钥,客户端使用签名密钥对请求参数进行签名,系统使用同样的算法对签名进行校验。
请求参数的签名算法如下:
①按参数名称的字母(区分大小写)顺序排列请求参数,如两个参数name和address:
②连接按参数名称排列的参数值:“江苏省南京市长江路69号”+“张三”=“江苏省南京市长江路69号张三”
③添加接入代码(以test为例),当前时间戳和签名密钥(以Doak!937为例):“test”+“江苏省南京市长江路69 号张三”+Sys?tem.currentTimeMillis()+“Doak!937”=“test江苏省南京市长江路69号张三1688434016264Doak!937”
④计算SHA256的哈希值(使用UTF-8编码),得到签名值:DigestUtils. sha256Hex(src. getBytes(StandardChar?sets.UTF_8))=“d0058403e6a1cf3f16e7e0187d4ec92c0da17d31ce3a483ab946c8dc9fa87cef”調用者在实际请求时,除原参数外添加接入代码(appCode) 、时间戳(time) 和签名值(sign) 构建完整的请求参数。
2) 流量管控
流量管控依赖历史调用统计数据,而接口调用为高频操作,如果实时地存取数据库中的统计数据,将执行大量的SQL请求,严重影响系统性能。
系统采用数据缓存保存调用统计数据,查询时直接使用缓存数据,操作成功或失败后更新缓存数据,通过独立的数据持久化线程,每分钟执行一次将最近一分钟的日志数据批量保存到数据表,达到削峰平谷的效果。当系统异常重启时,首先将数据库中历史数据加载到缓存,以保证流量管控的正确运行。
为控制流量日志数据表的大小,系统每天将超过时效的日志数据转储到历史数据表,供日志监控功能查询。
系统支持以分钟、小时及天为单位进行流量控制。系统为每对调用者/接口分别按需建立一组包含12个元素的循环队列,每个元素对应一个时间片,图4 和图5分别为分钟(单位秒)、小时(单位分)和天(单位小时)时间片划分。
系统记录循环队列列头位置以及对应的时间点,时间点计算公式如下:
分钟时间片时间点=System.currentTimeMillis()/1000/5小 时时间片时间点=System.currentTimeMillis()/
1000/60/5 天时间片时间点=System. currentTimeMillis()/1000/3600/2
当客户端请求到达时,系统计算当前时间对应的时间点,与队列头时间点进行比较,如果值不相同,则移动队列头到适当位置,同时清空过期的元素,最后更新队列头的元素值。
单机部署环境下,系统使用JVM内存保存时间片队列。
集群环境下,系统使用Redis的哈希表模拟循环队列[4],哈希表中命名为item0,...,item11的元素对应队列的第0到第11个元素,head表示队列头元素名,time 表示队列头对应的时间点,lock表示队列锁。当请求的时间点与队列头时间点不一致时,使用HSETNX指令锁定队列,更新队列数据后释放队列锁,为避免重复更新,锁定后进行二次检查。最后使用HINCBY指令增加统计记数。
3) 客户端地址的识别
集群部署使用负载均衡器场景下,使用HttpServ?letRequest 的getRemoteAddr 方法已经无法获得客户端原始IP地址,导致白名单检查错误。这就需要在系统部署时由负载均衡器在请求头中添加XForwarded-For记录通过负载均衡的地址列表[5],系统在解析请求头值后,获得客户端原始地址。
实际应用中,客户端(尤其是互联网客户端)可能会通过构造X-Forwarded-For请求头,将非法的地址伪装成合法的请求地址,为此系统设置可信负载均衡器地址参数,只认可X-Forwarded-For请求地址中可信负载均衡器地址的上一级地址。
3 结束语
实践表明,基于元数据的通用数据查询接口系统显著地提升了查询类接口对接效率,单个查询接口实现时长由以前的平均2个工作日缩短到半个小时,而且杜绝了直连数据库查询的现象,增强了系统的安全性。系统上线后,配置各类查询240个,日均访问量9 万余次,取得了良好的效果。