看完这篇,你的API服务设计能力将再次进化!
在当今快速演变的技术场景中,服务设计不仅仅是遵循通用的设计规范和最佳实践的问题,它更深层次地触及到如何在满足这些标准的同时,确保服务能够灵活适应未来的变化、满足用户的期望。
本篇文章旨在探讨在遵循通用设计规范之外,服务设计过程中需要考虑的关键因素。希望经过我们一系列的分享,能和大家一起讨论如何将API设计的易对接,易理解和易扩展。
系统介绍
服务路径和模块
服务路径从域名后开始,应有统一的一级路径,如domain/api/,这样做的好处是:将所有API请求集中到一个明确的路径下可以简化安全控制的实施。比如可以在NP或API网关层面上,针对特定路径应用特定的安全策略,如认证、授权、日志、限流和监控等,从而增强API的安全性。 再向后则是业务分组,比如订单/api/order,商品/api/product等,可以让调用方清晰明了此接口是属于哪个业务模块。 还要考虑各服务的粒度,比如:商品价格是做成单独的服务,还是耦合在商品详情接口直接吐出呢?订单物流信息应该包含在订单详情服务里吗等等。考虑这部分,需要以当前业务场景作为基础,拿VOP来说,因为有客户级别的商品变价消息存在,所以调用方在拉取到商品变价消息之后,可以单独查询价格服务更新自己本地存储的价格,而不用全量查询商品信息,所以解耦更优。在保证业务场景完整度的情况下,尽可能地为调用方提供可灵活组装的服务。
服务请求方式
服务出入参
API服务出入参的定义,尤其是对外开放服务的出入参,需要先考虑扩展性,防止后续扩展受限以至于不得已增加新的接口导致重复建设、接口混乱不清、同一个接口有好些大同小异的版本等问题;其次就是对于各字段的定义和说明,通用的规范不用赘述,想要强调的是: 为了后续的扩展性以及对调用方屏蔽服务内部变化,某些数字字段要优先设计成Long甚至于直接定义成字符串,防止服务内部变化(比如某些字段Integer不够存储需要转换成Long时),给调用方增加额外的改造工作量。 在某些情况下,字段可能需要区分“未设置”和“零值”这两种情况。此时,需要考虑将字段设计为包装类型,这样,null可以表示“未设置”,而具体的零值(如0、0.0等)则有其特定含义。 字段取舍:出参字段非必要不暴露,在满足接口功能要求的前提下,尽可能的减少字段暴露;对于一些敏感字段,比如客户地址,手机号等信息,更要仔细斟酌是否需要吐出该字段,如必需,则还需要考虑字段脱敏和加解密。 字段定义及说明:详细且合规的字段定义和说明可以降低调用方对于服务字段的理解成本,确保数据的一致性和准确性,常见的说明有以下几种:
字段长度的限制。 数字类型字段的范围。 各业务域相同含义的字段名保持一致。 字段格式说明:对于日期、时间等特殊格式的字段,明确其格式。 枚举类型的说明:对于枚举类型的字段,所有可能的枚举值,并对每个值的含义进行说明等等。
已发布服务需要新增出/入参:已经发布出去的服务,如果要新增出入参,需要考虑对调用方的影响,如序列化/反序列化,字段处理等等;若调用方在进行接口出参反序列化时使用的是严格模式,那服务端随意的新增 返回字段将会使调用方在服务出参数据反序列化时失败,进而影响到业务进行。我们可以在服务入参增加扩展字段,当调用方传入指定数据时,服务再在返回体中增加新的出参。 出参格式统一:对外服务要求有固定的格式,清晰的标明处理结果,状态码以及业务数据等字段,能让调用方清晰地进行业务处理以及异常处理,如:
{"success": true,"resultMessage": "success","resultCode": "0000","result": {}}
服务内业务处理的一些建议
对于服务逻辑处理来说,尽量支持批量数据的处理。对于调用方来说,可以将自身重心放在数据处理逻辑上,而不是如何高并发的调用服务来处理这部分数据,可以有效减轻服务方压力。如订单/商品查询,发票开具等服务。 做水平越权处理:防止调用方查询到不属于自身的数据,具体校验严格程度视业务而定。比如VOP对于大部分的查询服务,都允许属于同一合同下的不同pin互相查询数据(订单,发票等)。 对于重点业务逻辑需要多线程处理时,尽量使用单独的线程池,防止线程池内线程被其他业务抢占影响重要业务。
异常处理
对于开放服务,尤其是写操作的服务,定义明确的异常码是尤为重要的,调用方系统需要准确的错误码和错误信息来自动化地处理接口调用的结果:重试、告警或直接忽略、业务中断或继续进行。相信我们都遇到过调用一些没有任务错误码文档的接口,调用方完全不知道这个接口会抛出或者返回一些什么样的错误码以及错误描述,没办法决策这些异常是否可以展示给客户!(对于一些技术化的错误提示:比如'调用XX接口异常','XX数据为空'等等) 第3点时我们提到了服务的出参格式需要统一,所以在出现业务异常时,我们要包装合适的异常码以及异常信息返给调用方,另外还需要注意出现未知异常(空指针,调用依赖接口超时等)时,对出参的包装,防止出现直接抛异常给调用方的情况,可以在最外层使用拦截器做未知异常的兜底出参处理。
服务的强弱依赖
服务的监控和日志记录
日志记录:对于一个对外服务(尤其是直接面向外部客户的服务)来说,除了系统中常用的技术监控以外,服务出入参日志的记录是至关重要的一环,尤其是涉及资金、订单、支付等各种类型的写操作服务,一方面我们可以监控重要服务中,各调用方都使用了哪些字段,从而为在后续新增、下线或修改某些字段时提供风险评估数据;另一方面还可以从日志记录出入参中分析业务数据,为后续业务决策、业务告警等提供数据支撑。另外,当出现调用方和服务方数据不一致而产生严重后果的情况时,这些日志的记录也是判断问题点的重要信息。 调用数据收集:对于开放平台的流量细节做统计和分析,是做流量治理、保障服务安全的重要前提,通过对各调用方的调用分析可以得知调用方的调用频率、调用规律、请求是否合理等信息,对于一些非法调用(刷单、爬虫等)进行有效识别,可以统计出调用方的平均调用量进而为服务限流、安全防护等提供数据支持。
合理设置降级点
处理过时服务
若逻辑兼容,可以将旧服务路由到新服务 下线处理,但不建议直接删除代码,可以统一拦截需要下线的路径,给调用方返回统一的错误码和提示信息,一旦监控到拦截告警或者调用方反馈,可以先停止拦截并和调用方沟通后续处理方案。
不要相信任何调用方和依赖的第三方
敏感字段
传输层加密:使用SSL/TLS等协议对客户端和服务器之间的整个通信进行加密。这意味着所有传输的数据,包括敏感字段,都会被加密,从而保障数据在传输过程中的安全。 应用层加密:在数据发送之前,直接在应用层对特定的敏感字段进行加密。这通常涉及到使用加密算法(如AES、RSA等)对敏感数据进行编码。即便在传输层已经有加密,应用层加密也提供了另一层安全保障。 数据库层加密:对存储在数据库中的敏感字段进行加密,这种方式使用不多,一般用来保存密码等信息。
选择合适的加密标准和算法,确保是业内常用的、经过验证的。 管理好加密密钥,密钥的生成、存储、交换和销毁都应当安全可靠。 考虑性能影响,加密和解密操作会增加计算复杂度,需要确保服务性能不会因此受到明显影响。
系统准入
接口出入参防篡改
使用HTTPS
数字签名
消息认证码(MAC)
使用API密钥和时间戳
出入参加解密
/*** @description 指定客户接口出入参加解密拦截器*/@Servicepublic class EncryptInterceptor extends HandlerInterceptorAdapter {//请求进入,业务处理前解密@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if(!(request instanceof DecryptionRequestWrapper)){return true;}//获取客户授权信息,获取到当前客户身份try {//判断该客户是否开启接口加解密//组织参数,获取原始入参,进行解密操作//解密完成后,将解密得到的参数全部加入到request的参数内//继续向下进行return true;} catch (Throwable throwable){//异常处理}}//返回前加密@Overridepublic void postHandle(HttpServletRequest request,HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {//获取客户身份标识try{//判断该客户是否开启接口加解密//调用原始的 postHandle 方法,让响应数据被写入包装类的输出流//调用加密方法,获取加密后数据//将加密后的数据写回响应}catch (Throwable throwable){//异常处理}}}
- END -