跳至主要內容

Da-API平台-模拟接口

holic-x...大约 28 分钟项目Da-API平台

模拟接口

1.模拟接口调用请求

【1】创建模拟接口工程

新建api-platform-interface工程

​ 因为springboot3.0开始最低需要java17,使用官方的源只有17开始的版本了。如果无法勾选Java 8,则修改Server URL

​ 进入下一步,输入依赖搜索选择依赖:选择Spring Web;选择Lombok插件;选择Spring Boot DevTools;

​ SpringWeb:是 Spring Boot 的一个模块,主要用于创建基于 Web 的应用程序。

​ Lombok:是一个 Java 库,可以通过简单的注解来帮助我们消除 Java 代码的模板化,例如 getter 和 setter 方法、构造函数等。

​ Spring Boot DevTools:Spring Boot 的一个模块,主要用于在开发环境中提高开发效率。提供了自动重启、热交换、模板缓存等功能。

SpringBoot版本参考2.7.12

完成配置选择finish等待项目加载:(修改maven路径)

【2】接口开发构建思路

(1)通过URL访问调用接口

​ 提供3中接口供开发者模拟调用

【1】新建model层(创建model包),随后创建一个User类并定义属性username

@Data
public class User {
    private String username;
}

【2】新建controller层(创建controller包)

​ 创建NameController写3个模拟接口

/**
 * 名称 API
 */
@RestController
@RequestMapping("name")
public class NameController {
    @GetMapping("/getNameByGet")
    public String getNameByGet(String name) {
        return "GET 你的名字是" + name;
    }

    @PostMapping("/getNameByPost")
    public String getNameByPost(@RequestParam String name) {
        return "POST 你的名字是" + name;
    }

    @PostMapping("/getUserNameByPost")
    public String getUserNameByPost(@RequestBody User user) {
        return "POST 用户名字是" + user.getUsername();
    }
}

【3】将application.properties修改为application.yml配置文件,构建基础配置

# 应用服务 WEB 访问端口
server:
  port: 8080
  servlet:
    context-path: /api

【4】项目启动测试,模拟开发者调用接口:http://localhost:8123/api/name/?name=hello,通过浏览器访问得到响应信息

​ 目前已经成功地开发出了这个接口,但对于开发者来说,总不能每次都通过在浏览器地址栏输入接口地址来调用它,那么开发者通常是如何调用接口的呢?要么在前端进行调用,要么在后端,即我们的后端系统调用你的接口。出于安全考虑,我们通常会选择在后端调用第三方 API,因为这样可以避免在前端暴露诸如密码这样的敏感信息。因此需要进一步思考如何在在项目中调用第三方的接口。

​ 分别访问接口:http://localhost:8080/api/name/getNameByGet?name=x

(2)借助第三方工具在项目中调用接口

调用接口的方式(HTTP 调用方式)

  • HttpClient

  • RestTemplate

  • 第三方库(OKHTTP、Hutool)

​ 此处以Hutoolopen in new window为例,安装工具包依赖,借助HttpUtil工具类open in new window进行测试


			<!-- 引入hutool依赖 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>

【1】创建client包,创建MyClient.java,编辑类模拟调用第三方接口的客户端(此时转换角色,已经不是通过controller提供外部接口的服务端,而是借助HTTP调用第三方接口的客户端)

​ 调用接口可以参考hutool提供的官方文档进行参考:常见GET、POST、支持JSON类型的Restful请求

image-20240312210557136

package com.noob.apiinterface.client;

import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.noob.apiinterface.model.User;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.HashMap;

public class MyClient {
    // 使用GET方法从服务器获取名称信息
    public String getNameByGet(String name) {
        // 可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
        HashMap<String, Object> paramMap = new HashMap<>();
        // 将"name"参数添加到映射中
        paramMap.put("name", name);
        // 使用HttpUtil工具发起GET请求,并获取服务器返回的结果
        String result= HttpUtil.get("http://localhost:8080/api/name/getNameByGet/", paramMap);
        // 打印服务器返回的结果
        System.out.println(result);
        // 返回服务器返回的结果
        return result;
    }

    // 使用POST方法从服务器获取名称信息
    public String getNameByPost(@RequestParam String name) {
        // 可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
        HashMap<String, Object> paramMap = new HashMap<>();
        paramMap.put("name", name);
        // 使用HttpUtil工具发起POST请求,并获取服务器返回的结果
        String result= HttpUtil.post("http://localhost:8080/api/name/getNameByPost/", paramMap);
        System.out.println(result);
        return result;
    }

    // 使用POST方法向服务器发送User对象,并获取服务器返回的结果
    public String getUserNameByPost(@RequestBody User user) {
        // 将User对象转换为JSON字符串
        String json = JSONUtil.toJsonStr(user);
        // 使用HttpRequest工具发起POST请求,并获取服务器的响应
        HttpResponse httpResponse = HttpRequest.post("http://localhost:8080/api/name/getUserNameByPost/")
                .body(json) // 将JSON字符串设置为请求体
                .execute(); // 执行请求
        // 打印服务器返回的状态码
        System.out.println(httpResponse.getStatus());
        // 获取服务器返回的结果
        String result = httpResponse.body();
        // 打印服务器返回的结果
        System.out.println(result);
        // 返回服务器返回的结果
        return result;
    }
}

【2】编写测试类Main.java

package com.noob.apiinterface.test;

import com.noob.apiinterface.client.MyClient;
import com.noob.apiinterface.model.User;

public class Main {
    public static void main(String[] args) {
        MyClient myClient = new MyClient();
        String result1 = myClient.getNameByGet("noob");
        String result2 = myClient.getNameByPost("noob");
        User user = new User();
        user.setUsername("hello noob");
        String result3 = myClient.getUserNameByPost(user);
        System.out.println(result1);
        System.out.println(result2);
        System.out.println(result3);
    }
}

【3】启动测试

​ 先启动Application,确认前面定义的controller接口能够正常访问,随后启动Main.java测试,否则没法构建连接(理解为controller提供接口,Main模拟客户端请求接口)

2.API签名认证

【1】API签名基础概念

API签名认证概念理解

​ 基于上述模拟接口调用,需要思考一个重要问题:如果系统为开发者提供了一个接口,却对调用者一无所知。假设服务器只能允许 100 个人同时调用接口。如果有攻击者疯狂地请求这个接口,那将极其危险。一方面这可能会损害安全性,另一方面也可能耗尽服务器性能,影响正常用户的使用。

​ 因此,接口提供方必须为接口设置保护措施,例如限制每个用户每秒只能调用十次接口,即实施请求频次的限额控制。如果在后期业务扩大,可能还需要收费。因此,必须知道谁在调用接口,并且不能让无权限的人随意调用。

​ 现在,需要设计一个方法,来确定谁在调用接口。回想此前之前开发后端时,会进行一些权限检查。例如,当管理员执行删除操作时,后端需要检查这个用户是否为管理员。那么,如何获取用户信息呢?是否直接从后端的 session 中获取?但问题来了,当调用接口时有 session 吗?比如说,用户是前端直接发起请求而没有登录操作(没有输入用户名和密码),我怎么去调用呢?因此,一般情况下,会采用一个叫API签名认证的机制去实现鉴权。

​ API 签名认证:简单地说,如果你想来我家做客,我不可能随便让任何陌生人进来。所以服务提供方会提前给调用方发一个类似于请帖的东西,作为授权或许可证。当调用方来访问的时候,则需要带上这个许可证。某种角度上来说,这是一个唯一凭证,服务提供方可能并不认识调用方,但是通过这个许可证媒介来决定是否要给他提供服务

​ 所以,API 签名认证主要包括两个过程。第一个是签发签名,第二个是使用签名或校验签名。这就像一些短信接口的 key 一样。

​ 为什么需要API签名认证呢?简单地说,第一,为了保证安全性,不能让任何人都能调用接口。那么,如何在后端实现签名认证呢?需要借助 accessKey 和 secretKey来标识用户。这和用户名和密码类似,不过每次调用接口都需要带上,实现无状态的请求。这样,即使你之前没来过,只要这次的状态正确,你就可以调用接口。 ​ 下述案例介绍如何签发 accessKey 和 secretKey,以及如何使用和验证它们。在签发过程中,可以自己编写一个生成 accessKey 和 secretKey 的工具。一般来说,accessKey 和 secretKey 需要尽可能复杂,以防止黑客尝试破解,特别是密码,需要尽可能复杂,无规律。

API签名认证梳理

本质:签发签名、使用签名(校验签名)

为什么需要?:保证安全性,不能随便一个人调用;适用于无需保存登录态的场景。只认签名,不关注用户登录态。

【1】签名认证实现:

(1)通过 http request header 头传递参数。

  • 参数 1:accessKey:调用的标识 userA, userB(复杂、无序、无规律)
  • 参数 2:secretKey:密钥(复杂、无序、无规律)该参数不能放到请求头中

​ (类似用户名和密码,区别:ak、sk 是无状态的)

ps:可以自己写代码来给用户生成 ak、sk,千万不能把密钥直接在服务器之间传递,有可能会被拦截

  • 参数 3:用户请求参数
  • 参数 4:sign

(2)加密方式:

对称加密、非对称加密、md5 签名(不可解密)

用户参数 + 密钥 => 签名生成算法(MD5、HMac、Sha1) => 不可解密的值

abc + abcdefgh => sajdgdioajdgioa

怎么验证签名是否匹配:服务端用一模一样的参数和算法去生成签名,只要和用户传的一致就表示一致

(3)如何防重放?

  • 参数 5:加 nonce 随机数,只能用一次服务端要保存用过的随机数

  • 参数 6:加 timestamp 时间戳,校验时间戳是否过期

    API 签名认证是一个很灵活的设计,具体要有哪些参数、参数名如何一定要根据场景来。(比如 userId、appId、version、固定值等)

扩展问题

【1】难道开发者每次调用接口都要自己写签名算法?

【2】API签名认证实现

​ 在之前设计的用户表中,并没有包含 accessKey 和 secretKey。现在,假设为每个用户分配一个唯一的 accessKey 和 secretKey。这样,当用户调用接口时,只需携带 accessKey,后端就能知道是哪个用户在进行调用。(确认数据表user中有accessKey 和 secretKey,以api-platform-interface/sql/create_table.sql为参考)

​ 测试:创建用户,修改accessKey 、secretKey(这两个值可以通过代码生成)

❓思考:为什么需要两个 key?如果仅凭一个 key 就可以调用接口,那么任何拿到这个 key 的人都可以无限制地调用这个接口。这就好比,为什么你在登录网站时需要输入密码,而不是只输入用户名就可以了?其实这两者的原理是一样的。如果像 token 一样,一个 key 不行吗?token 本质上也是不安全的,有可能会通过重放等等方式来攻破的。

借助accessKey 、secretKey实现

(1)复制MyClient,构建MyClient2.java完善代码,借助accessKey、sercetKey完成接口调用

public class MyClient2 {

    private String accessKey;

    private String secretKey;

    public MyClient2(String accessKey, String secretKey) {
        this.accessKey = accessKey;
        this.secretKey = secretKey;
    }

    // 创建一个私有方法,用于构造请求头
    private Map<String, String> getHeaderMap() {
        // 创建一个新的 HashMap 对象
        Map<String, String> hashMap = new HashMap<>();
        // 将 "accessKey" 和其对应的值放入 map 中
        hashMap.put("accessKey", accessKey);
        // 将 "secretKey" 和其对应的值放入 map 中
        hashMap.put("secretKey", secretKey);
        // 返回构造的请求头 map
        return hashMap;
    }

    public String getUserNameByPostByValid(@RequestBody User user) {
        String json = JSONUtil.toJsonStr(user);
        HttpResponse httpResponse = HttpRequest.post("http://localhost:8080/api/name/getUserNameByPostByValid/")
                // 添加前面构造的请求头
                .addHeaders(getHeaderMap())
                .body(json)
                .execute();
        System.out.println(httpResponse.getStatus());
        String result = httpResponse.body();
        System.out.println(result);
        return result;
    }
}

(2)NameController.java新增getUserNameByPostByValid方法校验接口请求的accessKey、secretKey

@PostMapping("/getUserNameByPostByValid")
public String getUserNameByPostByValid(@RequestBody User user, HttpServletRequest request) {
    // 获取请求的的数据accessKey、secretKey校验
    String accessKey = request.getHeader("accessKey");
    String secretKey = request.getHeader("secretKey");

    if(!accessKey.equals("noob")||!secretKey.equals("abcdefg")){
        throw new RuntimeException("无权限请求接口");
    }

    return "POST 用户名字是" + user.getUsername();
}

(3)Main.java测试类中加入testClient2()方法测试API签名认证

public static void testClient2() {
        String accessKey = "noob";
        String secretKey = "abcdefg";
        MyClient2 myClient = new MyClient2(accessKey,secretKey);
        User user = new User();
        user.setUsername("hello noob");
        String result3 = myClient.getUserNameByPostByValid(user);
        System.out.println(result3);
}

​ 如果请求的accessKey、secretKey与API服务提供方保持一致,则允许接口调用,否则拒绝调用并抛出异常

【3】安全传递

安全传递讲解

​ 居于上述内容,可以思考一下,现在所做的事情是否存在问题?这种做法真的安全吗?尽管安全性可能略有提高,但是否涉及到破解的问题还不确定。那么现在问题出在哪里?是什么导致的?问题在于用户的请求有可能被人拦截,如果将密码放在请求头中,如果有中间人拦截到了你的请求,就可以直接从请求头中获取到密码,然后使用密码发送请求。

​ 需要注意的问题是,密码绝对不能传递。也就是说,在向对方发送请求时,密码绝对不能以明文的方式传递,必须通过特殊的方式进行传递。因此,目前的做法是行不通的。绝对不能直接在服务端或服务器之间传递密钥,这样是不安全的。那么,应该如何使其安全呢?在标准的 API 签名认证中,需要传递一个签名,类似传递一个许可证。不是直接将其传递给后台,而是根据该密钥生成一个签名。因为密码不能直接在服务器中传递,有可能会被拦截。

​ 所以需要对该密码进行加密,这里通常称之为签名。那么这个签名是如何生成的呢?可以将用户传递的参数(例如ABC参数)与该密钥拼接在一起,然后使用签名算法进行加密。但这里实际上并不是真正的加密,也可以使用加密算法。

​ 加密算法可以分为:单向加密(md5 签名)、对称加密、非对称加密

  • 对称加密是什么?它是分配一组密钥,可以加密和解密。

  • 非对称加密,可以使用公钥加密,私钥解密,有些情况下也可以使用私钥加密,公钥解密。

  • 单向加密,即加密后无法解密。这种是安全性最高的。

​ md5 本质上是一种签名算法。例如,百度网盘上传文件时,每个文件都有一个唯一的值。就像 md5,一般情况下是不可逆的,即无法解密。理论上,md5 这种方式最安全。所以思路不是给用户分配的密钥来进行加密,而是将其与用户的参数进行拼接。例如,密钥是 ABCDEFGH,经过签名算法后,最后得到的值可能是 fgthtrgerge。这个值是无法解密的。然后只需将此值发送给服务器,服务器只需验证签名是否正确即可。这样也不会暴露密码,因为根本不传递密码,而是根据密码生成一个值,然后将生成后的值传递给服务器。

​ 然而问题来了,既然这个值无法解密,作为我的 API 接口,服务端该如何知道传递的签名是否正确呢?如何判断?这个很简单,我可以再次使用相同的参数进行生成,并与你传递的参数进行对比,看它们是否一致。

​ 扩展一个问题:前端发送请求时是否可以直接加密传输?有一个问题,就是不要相信前端加密。前端的加密是有用的,但是前端的加密不能完全保证安全,所以不要依赖前端。之前说过所有的请求都是可以重放的。也就是说,无法直接加密,无论你如何加密,只要被人拦截了,他们只需使用你传递的加密内容再次发送给后台,结果是一样的。因此,实际上也不安全。现在讨论的这种方法还不是很安全,还存在更复杂的问题。

​ 因此,在 API 签名时,需要传递额外的参数,即签名。如何实现呢?通过 HTTP 请求头传递参数。另外,添加时间戳也不能防止重放攻击。那么如何生成签名呢?就像我刚刚提到的,将用户传递的参数以及其他一些内容使用签名算法转换为无法解密的值,并将该值传递过去。这本质上需要使用一个签名生成算法,这是关键。除了这些之外,刚刚提到,既然请求可以被拦截,即使加了签名,如果将该签名重放一次,后端也无法知道是中间人还是正常用户。

​ 那么如何解决这种重放攻击的问题呢?即如何防止他人复制并重复之前发布的请求呢?举个例子,假设你的电脑使用了代理、代理服务,请小心操作。在这种情况下,减少使用一些乱七八糟的操作,尤其是在公司进行开发时尽量少用代理。

安全传递实现

​ 上述API签名实现对应的客户端只有accessKey、secretKey参数,不免暴露接口调用过程中的一些问题,可以在此基础上再添加几个参数,完善API调用方和API服务提供方校验

(1)客户端请求:

需要指定什么参数:accessKey、secretKey、timestamp、sign、body,此处为了简单测试,暂时调整为自定义数据模拟调用,后续要考虑一些字段的数据化存储

  • accessKey:通过指定派发,存储在数据库或者文件中(服务端派发指定的用户的accessKey,提供给client调用请求)
  • secretKety:秘钥,一般不通过互联网传递(避免被拦截攻破加解密方式)(服务端接收到accessKey之后会根据用户提供的accessKey进行数据库检索,获取到调用者的所有信息)
  • timtstamp:时间戳,用于校验服务请求和服务接收的时间差,如果中间请求响应时间过长则有可能存在风险,这个时候服务端会选择拒绝这个请求以弱化风险
  • sign:签名,通过组合字段借助md5加密方式进行加密操作(此处设定通过body+secretKey的方式生成sign,只要两边生成的sign一致则sign校验通过)
  • body:用户请求的实体,一般包括相关的请求参数(类似用户id等属性),服务端接收到请求之后可以利用这些参数进一步强化校验
public class MyClient3 {

    private String accessKey;

    private String secretKey;
    private String body;

    public MyClient3(String accessKey, String secretKey, String body) {
        this.accessKey = accessKey;
        this.secretKey = secretKey;
        this.body = body;
    }

    // 创建一个私有方法,用于构造请求头
    private Map<String, String> getHeaderMap(String json) {
        Map<String, String> hashMap = new HashMap<>();
        hashMap.put("accessKey", accessKey);
        // 不能直接发送秘钥

        // 生成随机数(生成一个包含4个随机数字的字符串)
        hashMap.put("nonce", RandomUtil.randomNumbers(4));
        // 请求体内容
        hashMap.put("body",body);
        // 当前时间戳(System.currentTimeMillis(); 返回当前时间的毫秒数,通过初以1000将毫秒数转换为秒数,得到当前时间戳的描述;String.valueOf(); 方法用于将数值转化为字符串)
        hashMap.put("timestamp",String.valueOf(System.currentTimeMillis()/1000));
        // 签名(自定义签名工具类生成签名)
        String secretKey = "abcdefg";
        hashMap.put("sign", SignUtil.genSignByBody(body,secretKey));
        return hashMap;
    }

    /**
     * 通过POST请求获取用户名
     * @param user 用户对象
     * @return 从服务器获取的用户名
     */
    public String getUserNameByPostBySign(@RequestBody User user) {
        // 将用户对象转换为JSON字符串
        String json = JSONUtil.toJsonStr(user);
        HttpResponse httpResponse = HttpRequest.post("http://localhost:8080/api/name/getUserNameByPostBySign/")
                // 添加请求头
                .addHeaders(getHeaderMap(json))
                // 设置请求体
                .body(json)
                // 发送POST请求
                .execute();
        // 打印响应状态码
        System.out.println(httpResponse.getStatus());
        // 打印响应体内容
        String result = httpResponse.body();
        System.out.println(result);
        return result;
    }

}

​ 其中SignUtil签名工具类定义如下

/**
 * 自定义签名工具类
 */
public class SignUtil {

    /**
     * 定义生成签名的方法
     * @param hashMap 包含需要签名的参数的哈希映射
     * @param secretKey 密钥
     * @return 生成的签名字符串
     */
    public static String genSignByMap(Map<String, String> hashMap, String secretKey) {
        // 使用SHA256算法的Digester
        Digester md5 = new Digester(DigestAlgorithm.SHA256);
        // 构建签名内容,将哈希映射转换为字符串并拼接密钥
        String content = hashMap.toString() + "." + secretKey;
        // 计算签名的摘要并返回摘要的十六进制表示形式
        return md5.digestHex(content);
    }

    /**
     * 结合实际场景选择,可以根据hashMap集合生成sign(考虑客户端和服务端一些共同参数校验,所以用hashMap封装),但如果是一些用户参数,此处生成签名可以不需要难么多参数
     * @param body
     * @param secretKey
     * @return
     */
    public static String genSignByBody(String body, String secretKey) {
        // 使用SHA256算法的Digester
        Digester md5 = new Digester(DigestAlgorithm.SHA256);
        // 构建签名内容,将哈希映射转换为字符串并拼接密钥
        String content = body + "." + secretKey;
        // 计算签名的摘要并返回摘要的十六进制表示形式
        return md5.digestHex(content);
    }

}

(2)服务端校验

​ 服务端接收到这些请求,首先会依次获取到header的每个参数信息,随后根据既定规则依次校验数据一致性,只有所有校验规则都满足方能进行下一步操作。参考NameController实现

    // 3.API签名调用认证(根据accessKey、secretKey、timestamp、sign、body等多个属性组合认证)
    @PostMapping("/getUserNameByPostBySign")
    public String getUserNameByPostBySign(@RequestBody User user, HttpServletRequest request) {
        // 1.校验请求头传入的五个参数,例如accessKey可以模拟数据库中查一下
        // 从请求头中获取参数
        String accessKey = request.getHeader("accessKey");
        String nonce = request.getHeader("nonce");
        String timestamp = request.getHeader("timestamp");
        String sign = request.getHeader("sign");
        String body = request.getHeader("body");
        // 不能直接获取秘钥(不能直接传递secretKey),一般从服务器端获取到accessKey,通过该字段匹配数据库中关联的secretKey即可
        // String secretKey = request.getHeader("secretKey");

        // 2.校验权限,应该查询数据库验证权限(此处模拟校验)查验数据库中accessKey是否分配给用户
        if (!accessKey.equals("noob")){
            throw new RuntimeException("无权限");
        }

        // 3.校验一下随机数(后端存储用hashmap或redis都可以)(此处模拟校验,设置判断nonce是否大于10000)
        if (Long.parseLong(nonce) > 10000) {
            throw new RuntimeException("无权限");
        }

        // 4.校验时间戳与当前时间的差距(设定请求时间和当前时间间隔不能超过一定时间范围)
        if (System.currentTimeMillis()/1000-Integer.valueOf(timestamp)>300) {
            throw new RuntimeException("无权限");
        }

        // 5.校验生成签名
        String secretKey = "abcdefg";// 模拟数据,实际要从数据库中获取
        String serverSign = SignUtil.genSignByBody(body,secretKey);
        if(!sign.equals(serverSign)){
            throw new RuntimeException("无权限");
        }

        return "POST 用户名字是" + user.getUsername();
    }

​ 模拟问题:如果基于上述API签名认证方式,假设张三是一个攻击者,张三并不知道密码,所以张三会随便传递一些值作为密码去攻击接口调用会产生什么影响

​ 【1】由于张三不知道正确的密码(secretKey不会通过互联网传递出去),张三传递的密码与服务端生成的签名所使用的密钥肯定不一致,所以无法执行有效的操作。

​ 【2】由于在sign的生成规则是基于body+secretKey的方式生成,因此如果张三获取不到有效的secretKey或者不了解其中的sign生成机制,其客户端和服务端生成的签名必然是不同的。

​ 整个签名认证算法的流程大概如上面所示,但需要强调的是,API签名认证是一种非常灵活的设计,具体需要哪些参数以及参数名的选择都应根据具体场景来确定。尽量避免在前端进行签名认证,而是由服务端来处理。这里提供的是一种相对规范的设计方法。例如,某些公司或项目的签名认证可能会包含 userId 字段以区分用户。还可能包含 appId 和 version 字段来表示应用程序的版本号。有时还会添加一些固定的盐值等等,具体标准选择结合实际场景需求做调整

【4】问题扩展

​ 了解了上述API签名认证,此处要思考另一个问题,当要调用一个api服务接口的时候,客户端需要编写大量的重复代码,且每次调用接口都要处理繁琐的事情。需要自己生成时间戳,编写签名算法,生成随机数等等,这些都是相当繁琐的工作。

​ 因此,当构建接口开放平台时,需要想办法让开发者能够以最简单的方式调用接口。开发者只需要关心传递哪些参数以及他们的密钥、APP等信息。一旦告诉了他们这些信息,他们就可以轻松地进行调用了。

​ 对于具体的随机数生成和签名生成过程,开发者有必要关心吗?显然是不需要的。如果每次都要求开发者编写这么多代码,肯定会让他们感到沮丧。因此,我们需要为开发者提供一个易于使用的 SDK,使其能够便捷地调用接口。由此衍生后续SDK的开发调用

3.SDK开发

【1】Starter核心

为什么需要 Starter?

​ 理想情况:开发者只需要关心调用哪些接口、传递哪些参数,就跟调用自己写的代码一样简单。 开发 starter 的好处:开发者引入之后,可以直接在 application.yml 中写配置,自动创建客户端。

进一步说明:

​ 为了方便开发者的调用,不能让单独每次都自己编写签名算法,这显然很繁琐。因此,需要开发一个简单易用的 SDK,使开发者只需关注调用哪些接口、传递哪些参数,就像调用自己编写的代码一样简单。实际上,RPC(远程过程调用)就是为了实现这一目的而设计的。参考RPC,它就是追求简单化调用的理想情况。类似的例子是小程序开发或者调用第三方 API,如腾讯云的 API,它们都提供了相应的 SDK。 现在的问题是如何开发这样一个 SDK,其实很简单。这里为了让开发者更方便使用 SDK,给它提供一个 starter。

​ 以api-platform-backend 项目举例说明,找到 pom.xml:可以看到看到引入 mybatis、redis、 swagger 接口文档的时候,都使用了 starter

​ 使用 starter 之后,有哪些好处?比如,对于 Redis 的 starter,可以直接在 application.yml 配置文件中进行相关配置。可以在配置文件中简单地定义一个连接到 Redis 的配置块,或者对于 Swagger 接口文档,也可以在配置文件中进行相应的配置。这样做的好处是,无需手动编写繁琐的配置代码或者创建客户端实例。通过引入适当的 starter,就可以直接使用它们提供的代码和客户端。只需在配置文件中进行简单的配置,整个过程就自动完成了。

​ 这正是 starter 的作用所在。使用 starter 的好处就是,开发者引入后可以直接在 application.yml 中进行配置,自动创建相应的客户端。这样使得开发过程更加简单便捷,无需过多关注底层实现细节,而是专注于配置和使用。所以接下来就带大家来做这件事情,让编写的 starter 能够为开发者写配置时提供提示,并自动创建客户端(如下图所示的提示)。

【2】Starter开发流程

(1)api-platform-client-sdk工程构建配置

​ 选择依赖:Lombok、Spring Configuration Processor 后,点击Finish。

  • Spring Configuration Processor:它的作用就是帮助开发者自动生成配置的代码提示

​ 点击finish完成项目创建,随后配置maven仓库依赖

  • 修改pom.xml文件

a.确认sdk版本号(version)

b.清理掉测试依赖(如果没有影响可不做调整)

c.删除build构建(此处要构建的是一个依赖包,而不是直接运行jar的项目)

d.刷新maven重新构建工程

(2)项目构建思路

依赖构建

​ 项目创建完成之后会默认生成一个SpringBoot的主类(但是此处不是要运行一个web项目,而是直接提供一个现成的客户端对象给用户使用),目标是构建一个可用的客户端对象(参考前面的API签名认证中客户端的实现,其实现方式是手动创建一个新的对象实例),而在此处则是希望用户能够通过引入starter的方式直接使用客户端,而不需手动创建。

​ 此处可以删除SpringBoot生成的主类ApiPlatformClientSdkApplication(如果删除了主类则其对应测试包中的也要删除ApiPlatformClientSdkApplicationTests否则项目启动可能会报错),创建ApiClientConfig.java

// 通过 @Configuration 注解,将该类标记为一个配置类,告诉 Spring 这是一个用于配置的类
@Configuration
// 能够读取application.yml的配置,读取到配置之后,把这个读到的配置设置到我们这里的属性中
@ConfigurationProperties("noob.client") // 给所有的配置加上前缀为"noob.client"
// @Data 注解是一个 Lombok 注解,自动生成了类的getter、setter方法
@Data
// @ComponentScan 注解用于自动扫描组件,使得 Spring 能够自动注册相应的 Bean
@ComponentScan
public class ApiClientConfig {

    private String accessKey;

    private String secretKey;

}

​ 将上面API签名认证涉及到的客户端构建代码迁移过来(model实体定义、client请求定义、utils工具包)

​ 在pom.xml文件中引入hutool依赖,随后调整项目代码之间的应用和import问题

<!-- 引入hutool依赖 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>

​ 在ApiClientConfig中配置ApiClient对象

@Configuration
@ConfigurationProperties("noob.client")
@Data
@ComponentScan
public class ApiClientConfig {

    private String accessKey;

    private String secretKey;

    // 创建一个名为ApiClient的Bean
    @Bean
    public ApiClient apiClientConfig(){
        // 使用ak、sk构建一个ApiClientConfig实例
        return new ApiClient(accessKey,secretKey);
    }

}

​ 在resources文件夹下创建META-INF文件夹(如果没有resources则手动通过右键项目名进行创建),创建spring.factories文件并配置(此处对应配置ApiClientConfig的的引用)

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.noob.apiclientsdk.ApiClientConfig

# 此处注意是EnableAutoConfiguration而不是其他

​ 上述配置项指定了要自动配置的类(编写的配置类com.noob.apiclientsdk.ApiClientConfig)。通过在 spring.factories 文件中配置自定义配置类,Spring Boot 将会在应用启动时自动加载和实例化 ApiClientConfig,并将其应用于应用程序中。这样,就可以使用自动配置生成的 apiClient对象,而无需手动创建和配置。

​ 完成上述配置,刷新maven工程,随后执行install指令查看maven是否正常构建

​ 构建完成,查看日志中体现的依赖生成位置,进入maven仓库查看打包好的依赖(具体看自身项目maven仓库位置,根据工程定义对照查找即可)

项目引用测试

​ 可以在api-platform-interface工程中引入上面创建的依赖,然后再去模拟接口调用,刷新maven检查依赖是否正确引入

<!-- 引入自定义api-client-sdk -->
        <dependency>
            <groupId>com.noob</groupId>
            <artifactId>api-platform-client-sdk</artifactId>
            <version>0.0.1</version>
        </dependency>

​ 在application.yml文件中配置所需参数

​ 编写测试方法模拟调用(此处只需要关注client构建,直接通过其对象访问API接口)

@SpringBootTest
class ApiPlatformInterfaceApplicationTests {

    // 注入一个名为ApiClient的Bean
    @Resource
    private ApiClient apiClient;

    // 测试
    @Test
    void contextLoads() {
        // 创建一个User对象,并设置User对象的username属性
        User user = new User();
        user.setUsername("noob");
        // 调用apiClient的getUserNameByPost方法,并传入user对象作为参数,将返回的结果赋值给usernameByPost变量
        String result = apiClient.getUserNameByPostBySign(user);
        // 打印result变量的值
        System.out.println(result);
    }

}

​ 测试过程中发现访问失败,注入失败,一一排查依赖构建的过程发现在spring.factories配置文件中配置错误导致无法正常装配

​ 修改完成进行测试,测试通过

【3】Starter总结

​ 理解starter构建的关键步骤

(1)指定注册配置类,在其中定义要对外提供配置的参数

(2)META-INF文件夹下配置springboot.factories

(3)将SDK通过maven打包,外部项目通过引入依赖的方式直接引入(只需配置化,而无需关注内部的实现细节)

​ 为什么可以通过配置的方式引入,这个流程是怎么理解的。(理解配置名称、类型对应源码的位置)

​ 在api-platform-interface项目中的application.yml文件中配置参数,可以进一步查看依赖相关的内容,了解其实现逻辑

​ 而这个spring-configuration-metadata.json则是通过spring-boot-configuration-processor这个库生成的(参考api-platform-client-sdk的pom.xml引入)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

(4)如何将starter打包给其他人?

  • 可以将jar直接给他人,然后通过引入外部依赖的方式引入
  • 也可以将自己的maven发布到maven仓库中
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3