设计接口?这些要点你得拿捏住!

60 阅读24分钟

基础规范要点

入参合法性校验

入参的合法性校验是一个良好的接口必备的前提条件,通过入参的校验我们可以过滤掉许多无效的请求,提高系统的稳定性。我们可以将入参合法性校验分为常规性校验和业务校验。所谓的常规性校验包括:token 校验、必填校验、长度校验、类型校验等等;业务校验也就是特定业务场景下的校验,比如用户商品下单接口,那么下单金额一定要大于 0。

接口的版本控制

我们在开发小程序或者 app 的时候,都需要对版本进行升级,而提前设计好版本,可以避免因为升级导致旧的服务无法正常工作,我们要保证在升级的时候,新旧版本的服务都能正常运转。多版本控制现在比较常见的方式有:url 标识版本、header 标识版本、params 标识版本 3 种方式。

基于 url 标识版本不友好的点在于,对于前端工程师来说会比较麻烦,必须在每个请求的 URL 上标记一个版本号来使用对应的版本的 API。更友好的方式就是,前端只需要传一个当前 APP 的版本号,后端根据前端传过来的版本号,自动匹配到对应的版本 API 接口。

例如以下几种代码示例展现不同的版本控制方式:

//通过URL Path实现版本控制
@GetMapping("/v1/api/user")
public int right1() {
    return 1;
}
//通过QueryString中的version参数实现版本控制
@GetMapping(value = "/api/user", params = "version=2")
public int right2(@RequestParam("version") int version) {
    return 2;
}
//通过请求头中的X-API-VERSION参数实现版本控制
@GetMapping(value = "/api/user", headers = "X-API-VERSION=3")
public int right3(@RequestHeader("X-API-VERSION") int version) {
    return 3;
}

这三种方式中,URL Path 的方式最直观也最不容易出错;QueryString 不易携带,不太推荐作为公开 API 的版本策略;HTTP 头的方式比较没有侵入性,如果仅仅是部分接口需要进行版本控制,可以考虑这种方式。不过具体选用哪种方式还是要根据实际情况来定,同时要注意同一个功能的接口版本不宜过多,避免造成较高的维护成本,尽可能保证接口版本的稳定性和可读性(便于查找和排错)。

接口考虑幂等性

所谓幂等,指的是多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。我们在开发中主要操作也就是 CURD,其中读取操作和删除操作是天然幂等的,我们重点需要关心的就是创建操作、更新操作。创建操作一定是非幂等的因为要涉及到新数据的产生,而更新操作有可能幂等有可能非幂等,这个要看具体业务场景。

比如在商城下单场景中,若不实现幂等性控制,用户可能会因为同一笔订单提交了多次请求,进而产生多笔订单,这显然不符合业务需求。常见的保证接口幂等的解决办法有以下几种:

  • 唯一标识符:使用全局唯一 ID 作为请求标识,例如事务 ID 或请求流水号,确保每个操作都有一个唯一的参考键。在处理请求时,通过检查数据库中是否已存在此标识符来决定是否执行操作。
  • 数据库约束:设置唯一索引,如在订单表中对订单号做唯一约束,从而避免同一订单被创建多次。在更新操作中,结合条件更新语句,只有当满足特定条件时才会更新数据,以保证即使收到多次相同的更新请求,也只会执行一次有效更新。
  • 乐观锁 / 悲观锁
    • 悲观锁:在处理请求前获取资源锁,确保同一时间内只有一个请求能够修改数据。例如使用 “select * from xxx where id =1 for update;” 语句(注意 id 字段一定是主键或唯一索引,不然可能造成锁表的结果),且悲观锁使用时一般伴随事务一起使用。
    • 乐观锁:记录数据版本号,在更新时检查版本号是否一致,以防止并发更新导致的数据不一致。可以通过 version 或者其他状态条件来实现,如 “update table_xxx set name=#name#,version=version+1 where version=#version#”,更新操作最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表。
  • 业务状态校验:在执行操作之前或之后,验证业务对象的状态,如果发现状态已经改变到不应该再次执行该操作的情况,则拒绝执行。
  • Token 机制或滑动窗口:对于短时间内连续的重复请求,可以通过发放 Token 或者使用滑动窗口机制限制请求频率,超出频率范围的请求直接拒绝。
  • 异步处理与幂等消费:将非幂等的操作放入消息队列,确保消息队列消费者具备幂等消费的能力,即无论同一个消息被消费多少次,其结果都是一致的。
  • 事务补偿:如果发生了重复操作,可以通过回滚或者补偿交易的方式来恢复业务的一致性。
  • 客户端控制:前端可以采取防抖 (debounce) 或节流 (throttle) 的方式防止用户的快速重复点击,从而减少重复请求的发出。

接口考虑防止重复请求

防重和幂等的概念其实很像,上面保证幂等的解决方案同样适用于防止重复请求。它们的区别在于:防重的目的是防止重复数据的产生,比如新增接口,用户快速点击两次,如果没做防重,就会产生重复数据;而幂等是比如请求多次,只有第一次请求才会做数据处理,后面的请求不会产生数据改变,例如退款接口,第一次退款成功后,后面的请求,不会再次退款成功。

常见的防止接口重复请求的实现方式有多种,以下是一些示例:

  • 请求队列:维护一个请求队列,每次发送请求前检查队列中是否已经存在相同的请求。如果存在相同请求,则不再发送,直接使用队列中的请求结果。这种方法可以确保相同请求只发送一次。
  • 请求取消:在发送请求前,记录当前正在进行的请求,并在发送新请求时先取消之前的请求。可以使用 Axios 等库提供的取消请求功能来实现。
  • 防抖(Debounce) :使用防抖函数控制请求的发送频率,确保在一段时间内只发送一次请求,这样可以避免频繁的重复请求。
  • 节流(Throttle) :与防抖类似,节流函数可以控制一段时间内请求的频率,但不会像防抖那样在每次触发事件后立即执行,而是在固定间隔内执行一次。
  • 请求标识:为每个请求设置唯一标识,当新请求到来时,先检查是否存在相同标识的请求,如果存在则不发送新请求。
  • 缓存请求结果:对于相同的请求,在第一次请求返回结果后将结果缓存起来,后续相同的请求可以直接使用缓存的结果,而不再发送重复请求。
  • 使用状态管理库:在 Vue 应用中,可以结合状态管理库(如 Vuex、Pinia)来管理请求状态,确保只有一个请求在进行,避免重复请求。

性能提升要点

提高接口的响应时间

在设计接口时,提升其响应时间是至关重要的,这直接关乎用户体验以及系统的整体性能。以下是一些提高接口响应时间的有效办法:

数据库查询走索引:在非大数据(几万以上记录)的情况下,影响接口响应速度的因素中,数据库查询次数往往影响较大。一般一次数据库查询需要 50 毫秒以上,最快也要 20 毫秒。所以要尽量减少查询次数,特别是杜绝嵌套查询,把能合并的查询合并成一个,最后转成 map 再查找处理,这能大大减少响应时间。例如曾遇到一个嵌套查询用时 120 秒左右,优化后变成 1 - 2 秒。同时,对真正的大数据处理,并且不需要实时的,可以用定时任务处理后存入新表,使用时直接查新表。另外,在查询的字段上建立索引也很关键,曾有大数据查询,没建索引时用时 12 秒,建立索引后响应时间提高到 1.1 秒,可见索引作用之大。而且要做到使用什么字段就选择什么字段,尽量减少字段,这样给前端省流量并且能提高响应速度。

添加缓存:缓存可以减少对数据库或其他资源的访问次数,从而加快接口响应速度。比如对于那些不经常变化的数据,可以将其缓存起来,像在 Spring Boot 应用中,可使用@Cacheable注解对方法的返回值进行缓存,当参数相同时,就会直接从缓存中获取数据,而不用每次都去数据库或者远程服务取数据了。还可以利用浏览器缓存,通过设置适当的响应头,使浏览器对接口返回的数据进行缓存,这样下次请求相同接口时,浏览器可直接从缓存中获取数据,而无需再次请求服务器;对于一些频繁使用的数据,也可将其存储在浏览器的本地存储中,避免重复的请求。

采用异步操作:将一些耗时的操作异步执行,能有效提升接口的响应速度,这样就不会阻塞调用它的主线程了。例如在 Spring Boot 中可以使用@Async注解来标记一个方法为异步方法,像发送邮件、生成报表等耗时任务就可以采用异步方式处理。另外,在业务逻辑中,如业务数据审核通过后需要给用户发短信,发短信过程比较耗时,就可以让短信发送这一操作转为异步执行,解耦耗时操作和核心业务。

代码优化:优化代码结构和算法,避免重复计算和不必要的循环,也有助于提高接口响应速度。同时,在处理大量数据的接口中,使用数据压缩可以减少网络传输时间,例如在 RESTful API 中,可以通过压缩 JSON 或 XML 响应体来实现,像使用GZIPOutputStream对数据进行 GZIP 压缩,并在响应头中标明内容编码方式,以此提升响应速度。

负载均衡:在高并发情况下,使用负载均衡可以将请求分发到多个服务器上,避免单个服务器压力过大,从而提高接口的响应速度,让系统可以更好地应对大量用户请求。

减少 HTTP 请求次数:可以通过合并文件,将多个 JavaScript 或 CSS 文件合并为一个文件,减少页面加载时的请求数量;还可以对图片进行精简,优化图片大小,并使用 CSS Sprites 或图像瓦片来减少图片请求次数等方式,间接提升接口响应相关的整体性能,因为前端页面加载性能的提升也会影响到接口调用的整体体验和效率。

接口限流控制

接口限流对于维护系统的稳定性起着关键作用。在实际应用中,随着用户和客户端的增加,不适当的流量管理可能导致过载和服务中断。比如可能会遇到突发流量的情况,像受到 DDoS 攻击或广告宣传活动影响时,API 会受到突发的高请求量冲击;还有些不稳定的客户端,可能因为错误配置或恶意行为产生不合理的请求频率,这些都会对系统产生负面影响。而接口限流通过限制客户端可以发送到服务器的请求数量,控制流量的增长,防止过多的请求同时涌入服务器,导致服务器资源耗尽,降低稳定性,进而确保服务器能够更好地分配资源,保持响应速度稳定,保护服务器免受过度请求的影响,同时也能保障数据安全,避免因过多请求导致数据泄漏或滥用等情况发生。

在 Java 中,我们可以使用 redis 进行接口调用次数统计来限制接口调用频率。常见的实现方式有多种,以下介绍一种简单示例思路:可以由请求方 IP 组成 Redis 的 key,对请求次数进行自增操作,当达到设定的阈值时,即可抛出异常,提示请求者操作频繁。例如,限制某个 IP 在一定时间内(如 1 小时内)连续请求次数大于 5 次后拒绝该 IP 的请求,5 分钟后解除限制,代码层面可以结合自定义注解、AOP 切面编程等方式来给接口做调用频率限制,在切面逻辑中获取 IP 地址以及对应的 Redis 的 key,判断请求次数是否超限并做相应处理。

另外,还有不同的限流算法可供选择,比如:

  • 令牌桶算法:具有突发处理能力,可以应对突发流量,且能够平滑控制请求速率,适用于需要平滑控制请求速率的场景,例如 API 限流、网络带宽控制等,适合应对瞬时突发流量,同时维持长期平均速率稳定的情况。
  • 漏桶算法:无论请求速率如何,它以恒定速率输出请求,可以丢弃过多的请求,有一定的请求缓冲能力,适用于需要固定处理速率的场景,例如流量整形、数据包传输限制等,适合强制要求请求速率不超过某个固定值的情况。
  • 计数器限流:比较简单,适用于低负载场景,但不适合处理突发流量,常用于简单的请求速率限制,例如登录尝试次数限制。
  • 滑动窗口限流:可以适应不同的时间尺度,对突发流量有一定容忍度,能灵活地根据时间窗口调整限制,用于对请求速率有更灵活要求的场景,可以适应多个时间尺度的需求。

我们要根据具体的业务需求,包括对请求速率的要求、对突发流量的容忍度以及系统的稳定性需求等来选择合适的限流算法和相应的实现方式,并且在实施后,需要进行实时监控并根据实际情况调整限流策略,因为不同时间段可能需要不同的限制。

安全保障要点

黑白 IP 白名单

在接口设计中,IP 白名单和黑名单是保障接口安全的重要手段。

IP 白名单,指的是只允许设定的调用来源 IP 的请求被允许访问接口。例如,企业内部系统的一些接口,只有企业内部特定网段的 IP 地址可以访问,外部的 IP 则无法访问,这就可以通过配置 IP 白名单来实现。而黑名单与之相反,黑名单中 IP 的访问将被拒绝,像如果发现某些 IP 存在恶意攻击行为或者频繁异常访问,就可以将其加入黑名单,阻止其继续访问接口。相对来说,白名单的限制更为严格,它是一种正向的、明确允许的机制;而黑名单则是反向的,主要用于排除特定不受欢迎的 IP。

在实际设置 IP 白名单时,有多种方式可以选择。比如在 API 网关中,就提供了相应的 IP 访问控制插件来进行配置,支持配置 IP 或者 APPID + IP 的白名单访问模式,不在白名单列表的请求将会被拒绝,且配置时还可以选择 JSON 或者 YAML 格式来操作(两种格式的 schema 相同,若需转换可借助 yaml to json 转换工具)。像下面是一个简单的 YAML 格式配置示例:

type: ALLOW           # 控制模式,支持白名单模式'ALLOW'和黑名单模式'REFUSE'
resource: "XFF:-1"   # 可选字段,如果设置本字段,取X-Forwarded-For头中的IP作为判断依据,本示例取XFF头的倒数第一个IP作为客户端源IP判断
items:
  - blocks:         # IP地址段
    - 61.3.XX.XX/24   # 使用CIDR方式配置
    appId: 219810   # (可选) 如果配置了appId则该条目仅对该AppId生效
  - blocks:         # IP地址
    - 79.11.XX.XX    # 使用IP地址方式配置
  - blocks:         # 用户VPC
    - 192.168.XX.XX/32    # 专享实例场景,从用户VPC内访问API网关的请求,API网关看到的来源地址处于这个地址段

另外,在 Nginx 中也可以通过 allow、deny 配置项来实现黑白名单机制,例如:

allow xxx.xxx.xxx.xxx; # 允许指定的IP访问,可以用于实现白名单。
deny xxx.xxx.xxx.xxx; # 禁止指定的IP访问,可以用于实现黑名单。

若要同时屏蔽或开放多个 IP 访问时,为了避免配置冗余,还可以新建两个文件分别存放黑名单和白名单的 IP 配置。

推荐的做法是,根据接口的使用场景和安全需求合理选择黑白名单的配置方式。对于重要的、内部使用的接口,优先配置白名单,精准控制访问来源;对于可能遭遇恶意攻击的接口,结合黑名单及时阻断异常 IP 的访问,同时要定期审查和更新黑白名单列表,确保其有效性和安全性。

敏感数据脱敏

在接口调用过程中,往往会涉及到一些敏感字段,比如身份证号、手机号、银行卡号等,对于这些敏感数据,必须要做脱敏处理,以保护用户的隐私信息。

数据脱敏是指通过一定规则把敏感数据的实际值转换成虚构的值,整个过程是不可逆的,这样既能保证数据的隐私性,又能保证一定的数据可用性。例如,常见的脱敏规则如下:对于手机号码,通常会将最后 8 位以 “ ” 代替,像手机号 “13812345678” 脱敏后就会显示为 “138**5678”;身份证号码则一般将中间出生年月日部分以 “ ” 代替,比如 “110101199001011234” 脱敏后变为 “110101***1234”;门牌号码可以前三位以 “ ” 代替等等。

不同的项目和业务场景可能会有不同的脱敏要求和规则,开发人员需要根据实际情况进行相应的配置和处理,确保敏感数据在接口返回给客户端等环节中以脱敏后的形式呈现,防止隐私泄露带来的风险。

请求接口的先决条件 - token

在现代的接口设计中,token 机制是保障接口安全访问的重要一环。

当用户登录成功后,服务器会生成一个 token,这个 token 的生成通常会结合一些加密算法以及用户的相关信息来确保其唯一性和安全性。例如,可以使用像 JWT(JSON Web Token)这种常用的方式来生成 token,定义好 token 中包含的用户登录名、登录时间等关键信息以及设置合适的过期时间,并且通过特定的加密算法(如 HMAC256 等)和密钥进行签名生成最终的 token 字符串。生成后的 token 会存储在客户端,常见的存储位置像浏览器的 localStorage 中。

在后续用户进行接口请求时,需要在请求中携带这个 token,一般是放在请求头里(比如 “Authorization” 字段等),然后服务器端通过拦截器来校验这个 token 的有效性。服务器端会配置相应的拦截逻辑,拦截器获取到请求中的 token 后,按照事先约定好的验证算法(例如同样使用生成时的加密算法和密钥进行解析验证等)进行校验,如果 token 验证通过,说明该请求是合法的,来自已经登录认证过的用户,那么就可以继续处理该接口请求对应的业务逻辑;要是 token 验证不通过,比如 token 过期或者被篡改等情况,服务器则会拒绝该请求,并返回相应的错误提示信息给客户端,提示用户重新登录或者进行其他操作。

例如在 Spring Boot 项目中,可以通过自定义拦截器实现 HandlerInterceptor 接口,并在配置类中注册该拦截器,设定好需要拦截校验 token 的接口路径范围,像下面这样的简单代码示例:

@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        if (isValidToken(token)) {
            return true;  // 继续处理请求
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 返回401未授权
            return false;  // 拦截请求
        }
    }
    private boolean isValidToken(String token) {
        // 这里可以进行Token的解析和验证,例如使用相应的库和算法
        return token!= null && token.equals("VALID_TOKEN");
    }
}
@Configuration
public class IntercepterConfig implements WebMvcConfigurer {
    private TokenInterceptor tokenInterceptor;
    public IntercepterConfig(TokenInterceptor tokenInterceptor) {
        this.tokenInterceptor = tokenInterceptor;
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/api/**"); // 拦截所有/api前缀的请求
    }
}

通过这样一套 token 的生成、存储以及使用和校验机制,能有效地对接口访问进行权限控制,提升接口的安全性。

其他重要要点

记录接口请求日志

在接口设计中,记录接口请求日志是非常重要的环节,尤其是对于那些关键接口而言。通过记录请求日志,我们可以在后续遇到问题时,快速定位问题所在,也能在需要追溯操作、审计等场景下作为有力的证据。

不同的开发语言和框架都有相应记录日志的方式,比如在ASP.NET开发中,可以使用 HttpLogging 中间件来记录和处理接口请求日志;在 Spring Boot 项目里,能通过切面或者拦截器等方式实现日志记录功能。

日志中通常应包含请求的关键信息,像请求的 URL、请求时间、请求来源 IP、请求携带的参数、响应结果等内容。例如,在 Python 接口自动化测试中,可以利用 logging 模块构建日志记录系统,在实际的接口自动化测试代码里,通过 logger 实例记录 HTTP 请求的地址、方式、主体以及接口返回的状态码、信息等关键内容,方便后续查看分析。

有了这些详细的日志记录,一旦系统出现异常或者不符合预期的情况,我们就能依据日志所记录的时间线以及各请求的详细情况,快速排查是入参问题、逻辑处理问题还是其他方面的问题,及时解决,保障接口稳定可靠地运行。

接口单一职责

接口单一职责原则(Single Responsibility Principle,简称 SRP)是接口设计中一个很重要的准则,其核心定义为:应该有且仅有一个原因引起类(这里着重强调接口层面)的变更,也就是说一个接口只负责一项职责,只做一件事。

例如电话通话通常有拨号、通话、回应、挂机这 4 个过程,如果设计一个接口如下:

public interface IPhone {
    // 拨通电话
    public void dial(String phoneNumber);
    // 通话
    public void chat(Object o);
    // 通话完毕,挂电话
    public void hangup();
}

这样写虽然表面看没问题,但其实违背了单一职责原则,因为这个接口包含了两个职责:协议管理(dial () 和 hangup () 实现协议管理相关功能)和数据传送(chat () 实现数据传输相关功能),而协议管理和数据传输本是完全不相关的两个模块,彼此互不影响,各自变化的原因也不同。

更好的做法是把 IPhone 拆分成两个接口,这样设计后,一个类可以实现这两个接口,虽然从类角度看好像有两个原因引起变化,但面向接口编程对外公布的是接口,如此一来,接口的复杂性降低了,可读性提高了,可维护性也相应提高,而且变更引起的风险也随之降低。

不过在实际项目开发中,“职责” 的划分往往较难确定,因为并没有一个量化的标准去衡量一个类或者接口到底该负责哪些职责,这些职责又该细化到什么程度等,这都需要结合实际的项目情况,比如项目工期、成本、技术人员水平、硬件情况、网络情况等来综合考虑。但总体来说,建议接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

接口文档的可读性

接口文档是沟通前后端开发以及供其他相关人员了解接口使用方式的重要载体,良好的接口设计离不开清晰、详细且可读性优良的接口文档表述。

现在有多种工具和方式可以用来编写接口文档,例如 Swagger 就是常用的一种,它通过提供诸如 @ApiModel、@ApiModelProperty、@Api、 @ApiOperation、@ApiImplicitParams、@ApiImplicitParam 等注解,能更好更清晰地描述接口的请求参数、响应数据、数据模型等信息,帮助开发人员一目了然地知晓接口如何调用、有哪些参数限制、会返回什么样的数据结构等。

除了使用专门的工具配合注解的方式外,在文档书写格式上,markdown 语法也是不错的选择,它可以让接口文档简洁美观,使用 #表示标题层级、用不同符号标识列表、通过反引号展示代码等,使得文档内容条理清晰。

如果接口文档可读性差,那其他开发人员或者后续维护人员在使用接口时就容易产生误解,导致调用出错或者花费大量时间去研究接口的正确使用方法,所以在设计接口的同时,精心编写一份可读性高的接口文档是非常有必要的,这能极大地提高开发效率以及减少因沟通不畅等导致的问题