API 开放平台开发记录,关于 API 签名认证,开发sdk

API开放平台项目开发日志

项目介绍

一个提供 API 接口供开发者调用的平台。

参考案例:搏天免费api接口平台

需求分析

用户可以注册登录,开通接口调用权限。用户可以使用接口,并且每次调用会进行统计。

管理员可以对用户和接口进行增删改查,以及发布接口、下线接口、可视化接口的调用情况、数据。

  1. 增删改查
  2. 要有接口服务本身
  3. 管理员可以发布接口、下线接口
  4. 在线调用
  5. 统计调用次数
  6. 网关统一进行鉴权、日志、访问控制等

系统设计

整个系统分为五个模块:web系统,网关,模拟接口,客户端SDK,公共模块

技术选型

前端:

  • Ant Design Pro

  • React

  • Ant Design Procomponents

  • Umi

  • Umi Request (Axios的封装)

后端:

  • Java Spring Boot
  • Spring Boot Starter (SDK开发)
  • Dubbo
  • Nacos
  • Spring Cloud Gateway (网关、限流、日志实现)

项目计划

1. 初始化和展示

先搭建增删改查

管理员对用户和接口的增删改查,对接口的发布和下线

用户可以查看接口

2. 接口调用

用户可以在线调用

保证调用的安全性(API 签名认证)

接口文档展示

客户端 SDK(让用户更方便的调用)

3. 接口计费与保护

统计用户的调用次数

限流

计费?

日志

开通

4. 统计分析

提供可视化平台,用图表的方式展示所有接口的调用情况

前端初始化

ant design pro 官方文档:https://pro.ant.design/zh-CN/docs/getting-started/

安装依赖,npm install,运行访问

进行项目瘦身:

  • 移除国际化。运行package.json中的 “i18n-remove”: “pro i18n-remove --locale=zh-CN --write”,

    然后删除src/locales目录,删完了要重新运行看看,发现报错。问题出现在./src/components/index.ts文件中,尝试导出SelectLang时出现了问题。错误提示Export 'SelectLang' is not defined.表明在该文件中引用了一个未定义的导出SelectLangSelectLang是与国际化相关的组件,把它删除即可

  • 删除tests目录。用不到

重新 npm install

配置插件,使项目更加规范

  • 搜索 eslint 选上自动识别
  • 搜索 prettier 打√ 美化代码

后端初始化及库表设计

  1. 下载并打开 SpringBoot 项目初始模板,改项目名。或者手动创建springboot项目,引入依赖:spring web、Lombok、mybatis-plus(springboot3只需引入plus,不用单独引mybatis)、mysql …

  2. application.yml 配置 MySQL 数据源,idea连接数据源,建表—>MybatisX生成CRUD—>在controller里写增删改查接口

    user,用户信息表

    interface_info,接口信息表

    user_interface_info,用户调用接口关系

  3. 访问 localhost:7529/api/doc.html 就能在线调试接口了

OpenAPI 插件自动生成前端代码

https://pro.ant.design/zh-CN/docs/openapi

前端调用后端接口的代码,使用 oneapi 插件生成,只要将后端接口的 json 文档提供给前端(此文档需要遵循openapi规范)

具体操作,把后端接口的 json 文档地址配置到config/config.ts中,在package.json中执行openapi命令就能生成调用后端接口的代码。

config/config.ts

openAPI: [
    {
      requestLibPath: "import { request } from '@umijs/max'",
      schemaPath: 'http://localhost:7529/api/v3/api-docs',
      projectName: 'elijah-api-backend',
    },
  ],

将项目中的 requestErrorConfig.ts 改为 requestConfig.ts,然后在 app.tsx 找到 request 配置,将其修改成我们改的

app.tsx 是应用程序的入口文件,负责启动和配置整个应用。

image-20240407081022105

再打开 requestConfig.ts,修改名字,并设置一下后端地址

image-20240407081504120

但自动生成的代码需要手动修改才能用。

用户登录页面开发

找到src/pages/User/Login/index.tsx下的handleSubmit

  • 调用接口的方法名改成我们自动生成的

  • 用户名和密码字段名也保持和后端一致

记录用户的登录态

全局初始化数据:https://pro.ant.design/zh-CN/docs/initial-state

app.tsx

export async function getInitialState(): Promise<InitialState> {
  // 当页面首次加载时,获取要全局保存的数据,比如用户登录信息
  const state: InitialState = {
    loginUser: undefined,
  }
  try {
    const res = await getLoginUserUsingGET();
    if (res.data) {
      state.loginUser = res.data;
    }
  } catch (error) {
    history.push(loginPath);
  }
  return state;
}
const handleSubmit = async (values: API.UserLoginRequest) => {
    try {
      // 登录
      const res = await userLoginUsingPOST({
        ...values,
      });
      if (res.data) {
        const urlParams = new URL(window.location.href).searchParams;
        history.push(urlParams.get('redirect') || '/');
        setInitialState({
          loginUser: res.data
        });
        return;
      }
    } catch (error) {
      const defaultLoginFailureMessage = '登录失败,请重试!';
      console.log(error);
      message.error(defaultLoginFailureMessage);
    }
  };
<ProFormText
    name="userAccount"
    fieldProps={{
      size: 'large',
      prefix: <UserOutlined className={styles.prefixIcon}/>,
    }}
    placeholder={'用户名: admin or user'}
    rules={[
      {
        required: true,
        message: '用户名是必填项!',
      },
    ]}
    />
    <ProFormText.Password
    name="userPassword"
    fieldProps={{
      size: 'large',
      prefix: <LockOutlined className={styles.prefixIcon}/>,
    }}
    placeholder={'密码: ant.design'}
    rules={[
      {
        required: true,
        message: '密码是必填项!',
      },
    ]}
    />

发现刷新一下发现又要重新登录,推测是前端向后端发送请求的时候没有带上cookie

找到requestConfig.ts,添加 withCredentials: true,

export const requestConfig: RequestConfig = {
  baseURL: 'http://localhost:7529',
  withCredentials: true,
  // ...
}

刷新测试一下 问题解决

image-20240407163831057

查看控制台是否成功响应:

image-20240407164146340

前端请求接口,成功返回响应数据,但页面报错,为什么?

请求参数不对

响应不对

image-20240407164250865

image-20240407164342551

关于DTO对象

Data Transfer Object,数据传输对象。

  • 安全性。可以根据需要,仅传输所需的数据,避免将敏感数据传输到客户端。
  • 提高性能。减少了数据传输量,提高了性能。
  • 解耦。使用DTO可以减少各个层(比如控制器、服务层、持久层)之间的耦合度,因为DTO可以隐藏底层数据结构的细节,使得对数据的修改不会影响到整个应用程序。

开发模拟接口

elijah-api-interface

API 接口调用方式

  1. HttpClient

  2. RestTemplate

  3. 第三方库(OKHttp、Hutool)

Hutool的Http客户端工具类-HttpUtil:https://hutool.cn/docs/#/http/Http%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%B7%A5%E5%85%B7%E7%B1%BB-HttpUtil

API签名认证

为什么需要做签名认证?

一般情况访问web项目,需要先登录—>保存用户信息到session—>做校验,但这个项目无法做到,因为用户可以通过下载SDK调用接口,不用登录,所以为了防止接口被恶意调用,要设计 API 签名认证,为用户分配唯一accessKey,secertKey来鉴权,保障调用的安全性、可溯源性(指便于统计接口调用次数)。

具体操作

  1. 签发签名

    就是给每个用户一个ak和sk ----> 在user表中添加两个字段:

    accessKey,调用标识(复杂,无序,无规律)

    secretKey,密钥(复杂,无序,无规律)

    这俩类似用户名和密码,区别在于accessKey、secretKey是无状态的,就是不管你之前有没有调用,我只关心你这次有没有携带ak,sk。而用户名和密码是可以一次登录,之后一段时间保存登录态的

  2. 校验签名

    image-20230325141204243

    用户去发请求调用接口时,在requestHeader中携带一些参数(accessKey,nonce,body,timestamp,sign)

    把用户请求中的参数拿出来做校验,校验通过就能使用接口。

    这五个参数中 sign 是什么?为什么不是secretKey而是sign呢?

    因为密码不能直接在服务器间传递。如果直接把密钥(secretKey)放在请求头里,万一用户的请求被拦截,secretKey会直接暴露。所以要用secretKey 加上一个字符串,用算法生成一个不可解密的值—>签名sign。服务端用一模一样的参数和算法去生成 sign,生成出来的sign和用户传的一致,就可以调用。

    secretKey密钥 + 用户参数 —> 签名生成算法(MD5,HMac,Sha1) ----> 签名sign

    加密方式有:对称加密,非对称加密,md5 签名(单向加密,不可解密,最安全)

    nonce随机数和timestamp时间戳的作用是什么?

    是为了防止请求被拦截,拿sign重放。随机数只能用一次(每次请求后端要保存一下,下次请求来如果发现和上次的随机数一致,就不让它通过),加上检验它的有效期,就能保证sign每次都不一样。

开发客户端调用的SDK

工具类,作用是发送Http请求到到API网关,用户可以下载引入这个SDK,更方便调用接口
ak / sk在注册账号时分配给用户,由用户自己配置

为什么要开发SDK?

为了方便用户调用接口,给用户提供SDK(工具类),让用户用最少的代码调用我们的接口,不必让用户麻烦的写HttpClient,输入密钥,自己封装签名信息等

因为现在用户想要调用接口,需要自己发送http请求,其中要自己封装签名才能调用接口,所以为了解决用户调用成本过高的问题,基于 Spring Boot Starter 开发客户端 SDK,让用户引入依赖后,写一点配置,只需要一行代码即可调用接口。让用户只关心调用哪个接口、传递哪些参数即可

具体操作

  1. 新建项目,引入以下依赖:

    Lombok

    Spring Configuration Processor(让开发者在写配置时有提示。因为开发者在使用SDK的时候要写配置,这个依赖的作用就是自动生成配置的代码提示)

    写个version版本号,比如0.0.1

    然后删除pom.xml里里的内容,这个一定需要删除,因为这个是maven构建可运行的jar包用的。而现在是制作starter依赖包。

    然后删除运行主类,因为这个项目是作依赖包的使用的,不需要运行

  2. 写客户端类的代码,作用是发送Http请求到API网关

    /**
     * 客户端,作用是发送Http请求到API网关
     */
    public class ApiClient {
    
        /**
         * 网关地址
         */
        public static final String GATEWAY_HOST = "http://localhost:8090";
    
        private String accessKey;
        private String secretKey;
    
        public ApiClient(String accessKey, String secretKey) {
            this.accessKey = accessKey;
            this.secretKey = secretKey;
        }
    
    
        public String getNameByPostWithJson(User user) throws UnsupportedEncodingException {
            String json = JSONUtil.toJsonStr(user);
            HttpResponse httpResponse = HttpRequest.post(GATEWAY_HOST + "/api/user")
                    .addHeaders(getHeader(json))
                    .body(json)
                    .execute();
            return httpResponse.body();
        }
    
        public String getFriendCircleText() throws UnsupportedEncodingException {
            HttpResponse httpResponse = HttpRequest.get(GATEWAY_HOST + "/api/characters")
                    .addHeaders(getHeader(""))
                    .body("")
                    .execute();
            return httpResponse.body();
        }
    
        /**
         * 随机返回抖音美女视频
         * @return
         * @throws UnsupportedEncodingException
         */
        public String getGirlVideo() throws UnsupportedEncodingException {
            HttpResponse httpResponse = HttpRequest.get(GATEWAY_HOST + "/api/girl/video")
                    .addHeaders(getHeader(""))
                    .body("")
                    .execute();
            String body = httpResponse.body();
            String result = body.replaceAll("\\\\", "").replaceAll("//","https://");
            return result;
        }
    
        /**
         * 获取本地ip
         * @return
         * @throws UnsupportedEncodingException
         */
        public String getLocalIP() throws UnsupportedEncodingException {
            HttpResponse httpResponse = HttpRequest.get(GATEWAY_HOST + "/api/localip")
                    .addHeaders(getHeader(""))
                    .body("")
                    .execute();
            return httpResponse.body();
        }
    
        /**
         * 给请求头添加 accessKey 和 secretKey
         * @param body ->json,为了结合secret加密后做签名
         * @return Map集合,因为HuTool的HttpRequest的.addHeaders()需要的参数是map
         */
        private Map<String,String> getHeader(String body) throws UnsupportedEncodingException {
            Map<String,String> header = new HashMap<>();
            header.put("accessKey",accessKey);
            String nonce = RandomUtil.randomNumbers(5);
            header.put("nonce", nonce);
            header.put("body", URLEncoder.encode(body, StandardCharsets.UTF_8.name()));//处理中文乱码
            header.put("timestamp",String.valueOf(System.currentTimeMillis()));
            header.put("sign", SignUtil.getSign(body,secretKey));
            return header;
        }
    }
    
  3. 编写配置类

  4. 在配置文件中指定配置类

    新建配置文件:resources/META-INF/spring.factories

    # 开启配置类
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.wzg.clientsdk.ApiClientConfig
    
  5. 发布starter。双击Maven lifecycle下的install或者命令行mvn install

  6. 测试

为了避免客户端SDK源代码中的API网关地址被暴露,可以采用以下方法来处理:

  1. 将API网关地址配置放在配置文件中,在SDK编译阶段打包进去。在使用SDK的时候,只需要在集成的位置进行配置即可。
  2. 利用模板生成工具,在使用SDK时,将模板动态加载SDK中的API网关地址所在文件寻址进去。

这样,就可以保证在SDK开发中API网关地址不会被暴露出去。同时,还可以在必要时对API网关地址进行更新,而无需重新编译SDK。对于这些方式,还需要对配置文件和模板文件进行加密或加密处理来保证其安全性,以防止被恶意获取或者篡改。

发布接口

权限校验:该功能仅管理员可用

  1. 校验该接口是否存在
  2. 判断接口是否可以被调用(通过SDK发送请求,到达API网关,由网关调用模拟接口)
  3. 修改数据库接口字段 status 为 1

下线接口

权限校验:该功能仅管理员可用

  1. 校验该接口是否存在
  2. 修改数据库接口字段为 status 为 0

在线调用

想要在线调用,用户需要有签名,所以在用户注册的时候给他分配一个签名(生成accessKey、secretKey)一起存到用户表中(这里可以做扩展功能:用户可以申请更换签名)

实现在线调用功能,这里走后端调用会更安全更规范,调用前可以做校验,同时模拟接口的地址也不会暴露出来

image-20230402180736977

  1. 前端将用户要测试的接口id和输入的请求参数和发给平台后端(id + 请求参数的DTO对象)

  2. 后端做调用接口前的校验

    1. DTO对象feinull,其中的id非null

    2. 拿id去查,看接口存不存在

    3. 查接口的状态,是否已上线

  3. 后端去调用模拟接口

    1. 获取当前用户
    2. 拿到ak / sk
    3. 调用SDK

用户有接口id,参数,想调用这个接口

有几种HTTP的调用方式:

  1. HttpClient
  2. RestTemplate
  3. 第三方库(OKHttp,Hutool)

本项目使用Hutool提供的工具类调用,具体可看官网文档

image-20240414083451764

  • 21
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值