项目2:API Hunter 细节回顾 -1

news/2024/10/5 13:06:59/

一. 接口调用

对于开发者来说,接口的调用应当是方便快捷的,而且出于安全考虑,通常会选择在后端调用第三方 API,避免在前端暴露诸如密码的敏感信息。

若采用 HTTP 调用方式:

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

项目中使用了 Hutool 工具库中的 Http 客户端工具类,快速调用其它的 http 请求。

Hutool 工具库官方依赖:

<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version>
</dependency>

三个接口:

java">/**** 查询用户名称的 API**/
@RestController
@RequestMapping("name")
public class NameController {@GetMapping("/") public String getNameByGet(String name) {return "GET 你的名字是" + name;}@PostMapping("/") public String getNameByPost(@RequestParam String name) {return "POST 你的名字是" + name;}@PostMapping("/user") public String getUserNameByPost(@RequestParam User user) {return "POST 用户名字是" + user.getUsername();}
}

调用第三方接口,直接使用Http 客户端工具类官方代码,略微修改即可: 

java">/****调用第三方接口的客户端**/
public class kApiClient {// 使用 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:8123/api/name",paramMap);// 打印服务器返回结果System.out.println(result);// 返回服务器返回结果return result;}// 使用 POST 方法从服务器获取名称信息public String getNameByPost(@RequestParam String name) {// 可以单独传入 http 参数,这样参数会自动做 URL 编码,拼接在 URL 中。hashMap<String, Object> paramMap = new HashMap<>();// 将 "name" 参数添加到映射中paramMap.put("name", name);// 使用 HttpUtil 工具发起 GET 请求,并获取服务器返回结果String result = HttpUtil.post("http://localhost:8123/api/name",paramMap);// 打印服务器返回结果System.out.println(result);// 返回服务器返回结果return result;  }// 使用 POST 方法向服务器发送 User 对象,并获取服务器返回结果public String getUserNameByPost(@RequestParam User user) { // 将 User 对象转换为 JSON 字符串(轻量,方便在客户端服务端之间传输数据)String json = JSONUtil.toJsonStr(user);// 使用 HttpRequest 工具发起 POST 请求,并获取服务器的响应HttpResponse httpResponse = HttpRequest.post("http://localhost:8123/api/name").body(json) // 将 JSON 字符串设置为请求体.execute(); // 执行请求 // 打印服务器返回的状态码System.out.println(httpResponse.getStatus());// 打印服务器返回结果System.out.println(result);// 返回结果return result;}
}

二. API 签名认证

这部分在 API Hunter — 客制化API开放平台 一文中进行过阐述,此处重新整理并再次回顾。

1. 为什么需要认证

思考一个问题:如果我们为外界提供了一些可用接口,却对请求调用者一无所知。

这可能会让我们面临严重的安全问题。假设服务器最多只允许100人同时调用,有攻击者疯狂地请求接口,刷量刷请求,会严重消耗服务器性能,影响正常用户的使用,并且也会使损害系统安全。

因此,我们需要为接口设置保护措施,例如限流,限制每个用户每秒只能调用接口十次等。同时我们需要知道调用者信息,即谁在请求调用接口。类似于管理系统中的权限检查,执行删除操作时,后端会先去检查用户是否具有管理员权限等。

但用户调用接口时,可能是从前端直接发起请求,用户没有登录操作,也就不涉及用户名和密码,所以后端无法从 session 中获取用户信息,因为根本就没有。因此在这种情况下,就采用 API 签名认证机制

2. 什么是 API 签名认证

通俗地讲,就是基于授权(许可证)的身份校验

举例:客人想要参加我的宴会,需要有我事前签发的请帖,作为授权或许可证。当客人赴约时,需要带上请帖,只要有请帖,就能参加。

API 签名认证的过程:签发签名 -> 校验签名。

API 签名认证不仅保证了安全性,不让随便一个人调用接口,而且实现了用户的无状态请求,即只认签名,不关注用户登录态。

3. 涉及的参数和组件

通过 http request header 头传递参数。

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

认证过程中主要依靠签发 aK 和 sK 来完成身份的校验。类似于用户名和密码,只是是无状态的。

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

加密组件:

利用用户参数和密钥,然后通过签名生成算法(MD5、SHA256等)加密,变成不可解密的值,防止泄露和被破译。

用户参数 + 密钥 \overset{MD5}{\rightarrow} 不可解密的值

防重放:

  • 参数5:nonce 随机数,只能用一次。服务端保存用过的随机数。
  • 参数6:timestapm 时间戳,校验时间戳是否过期。

API 签名认证过程相对灵活,具体的参数应当根据实际业务场景合理选择。

4. 基本流程实现

首先给数据库中的用户表增加两个字段 accessKey 和 secretKey。

aK 和 sK 的生成通常要求无规律且复杂,为了模拟效果,此处先自行设置。

aK:khr123   sK:1q2w3e4r

Tips:之所以需要两个 key,还是为了保证安全性。就像在登陆网站时不仅需要用户名还需要密码。如果只有一个,那么任何一个拿到这个 key 的人都可以调用接口,不安全。

然后在调用接口客户端中增加这两个字段及其构造方法:

java">/****调用第三方接口的客户端**/
public class kApiClient {private String accessKey;private String secretKey;public KApiClient(String accessKey, String secretKey) {this.accessKey = accessKey;this.secretKey = secretKey;}……}

之后在调用的 KApiClient 的地方,将 aK、sK 拿到即可,客户端改造完成:

java">public class Main {public static void main(String[] args){ String accessKey ="khr123";String secretKey ="1q2w3e4r";KApiClient kApiClient = new KApiClient(accessKey, secretKey);……}
}

接下来服务端要校验 aK、sK,以 getUserNameByPost 接口为例说明:

首先要获取到用户传递的 aK、sK。这种数据建议不要直接在 URL 中传递,而是在请求头中传递更为妥当。因为 GET 请求的 URL 存在最大长度限制,如果传递的其它参数过多,会导致关键数据被挤出,所以建议从请求头中获取这些数据。

java">@PostMapping("/user")
public String getUserNameByPost(@RequestBody User user, HttpServletRequest request) {// 从请求头中获取名为 "accessKey" 的值String accessKey = request.getHeader("accessKey");// 从请求头中获取名为 "secretKey" 的值String secretKey = request.getHeader("secretKey");// 如果 accessKey 不等于 "khr123" 或者 secretKey 不等于 "1q2w3e4r"if(!accessKey.equals("khr123")||!secretKey.equals("1q2w3e4r")){// 抛出运行异常,提示权限不足throw new RuntimeException("无权限");}// 如果权限校验通过,返回"POST 用户名是" + 用户名return "POST 用户名是" + user.getUsername();
} 

其实在实际应用中,后端应该根据提供的 key 去数据库中查询,检查对应的用户是否合法或者该 key 是否被分配过等,此处仅作模拟。

java">/****调用第三方接口的客户端**/
public class kApiClient {private String accessKey;private String secretKey;public KApiClient(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);// 返回构造的请求头 mapreturn hashMap;}// 使用 POST 方法向服务器发送 User 对象,并获取服务器返回结果public String getUserNameByPost(@RequestParam User user) { // 将 User 对象转换为 JSON 字符串(轻量,方便在客户端服务端之间传输数据)String json = JSONUtil.toJsonStr(user);// 使用 HttpRequest 工具发起 POST 请求,并获取服务器的响应HttpResponse httpResponse = HttpRequest.post("http://localhost:8123/api/name")// 添加构造的请求头.addHeaders(getHeaderMap()).body(json) // 将 JSON 字符串设置为请求体.execute(); // 执行请求 // 打印服务器返回的状态码System.out.println(httpResponse.getStatus());// 打印服务器返回结果System.out.println(result);// 返回结果return result;}
}

测试后发现能够获取到 aK、sK,并且通过了验证。如果将 secretKey 随意修改为其它值,则会提示无权限:

java">hashMap.put("secretKey", "qweasdax");

5. 安全传递

虽然目前实现了通过签发 aK、sK 进行身份校验,但依然存在安全隐患。因为发送的请求可能被拦截,而我们传递的参数信息均放在请求头中,所以如果请求被拦截,那么攻击者可以直接从请求头中获取到密钥,然后使用密钥发送请求。

此外,secretKey 绝对不能传递,或者说不能以明文的形式直接传递,需要经过加密处理。在标准的 API 签名认证中,通常需要传递一个签名。签名是由用户传递的参数和 secretKey 拼接,并经过签名算法加密生成的。

加密算法通常又对称加密、非对称加密、单向加密等,详细内容可参考:一文读懂密码学

在项目中使用了 MD5 签名算法,即单向加密。这种加密方式不可逆,无法解密,通常用来生成签名。将生成的签名发送给服务器,服务器只需验证签名是否正确即可,这样根本不会暴露密码。

对于服务器而言,它会使用相同的参数与加密算法再次生成签名,以检验签名是否正确。

但这样做可能仍然存在被重放攻击的风险,所谓重放攻击,就是攻击者复制并重复之前发布的请求。也就是说即使攻击者不知道签名内容,将其拦截后,再次以请求者的身份发送给后端,依然能够完成调用。所以,为了避免重放攻击,再增加两个参数,分别是 nonce 随机数和 timestamp 时间戳

nonce 随机数:每次请求时,发送一个随机数给后端。后端只接受并认可该随机数一次,如果请求中带有重复的随机数,则不会处理请求。但这样会带来额外的存储开销,因为后端需要记录所有随机数。

timestamp 时间戳:每个请求在发送时携带一个时间戳,并且后端会验证该时间戳是否在指定的时间范围内,例如不超过 10 分钟,这样就可以防止攻击者使用先前的请求进行重放。可以和随机数配合使用,能够在一定程度上控制随机数的过期时间,后端也就不需要保存所有的随机数,减轻了存储开销。例如,只需要保存 10 分钟以内的随机数,因为后端需要校验随机数和时间戳两个参数,只要其中一个不符合要求,直接拒绝请求。

因此,在项目的签名认证算法中,至少需要添加五个参数:accessKey、secretKey、sign、nonce、timestamp。其它参数,比如接口的 name 参数等也可以添加到签名中,根据具体业务情况选择,以增加安全性。

无论如何,要确保密码绝不能在服务器直接传输,任何在服务器之间传输的内容都有可能被拦截。

6. 安全传递实现

首先在客户端中增加新的参数:

java">private Map<String, String> getHeaderMap() {Map<String, String> hashMap = new HashMap<>();hashMap.put("accessKey", accessKey);// 记住!不能直接发送密码// hashMap.put("secretKey", secretKey);// nonce 随机数hashMap.put("nonce", RandomUtil.randomNumber(4));// 请求体内容hashMap.put("body", body);// timestamp 当前时间戳hashMap.put("timestamp", String.valueof(System.currentTimeMillis() / 1000));// 生成签名hashMap.put("sign", genSign(body, secretKey));return hashMap;
}// 使用 POST 方法向服务器发送 User 对象,并获取服务器返回结果
public String getUserNameByPost(@RequestParam User user) { // 将 User 对象转换为 JSON 字符串(轻量,方便在客户端服务端之间传输数据)String json = JSONUtil.toJsonStr(user);// 使用 HttpRequest 工具发起 POST 请求,并获取服务器的响应HttpResponse httpResponse = HttpRequest.post("http://localhost:8123/api/name")// 添加构造的请求头.addHeaders(getHeaderMap(json)).body(json) // 将 JSON 字符串设置为请求体.execute(); // 执行请求 // 打印服务器返回的状态码System.out.println(httpResponse.getStatus());// 打印服务器返回结果System.out.println(result);// 返回结果return result;}

然后把用户参数进行拼接,经过签名算法生成唯一的字符串:

使用了 Hutool 工具库中的加密算法工具类(摘要加密),直接将生成签名当作一个工具类使用,

java">/*** 签名工具*/
public class SignUtils {/*** 生成签名* @param body 请求体内容(用户参数)* @param secretKey 密钥* @return 生成的签名字符串*/public static String genSign(String body, String secretKey) {// 使用 MD5 算法的 DigesterDigester md5 = new Digester(DigesterAlgorithm.MD5);// 构建签名内容,将哈希映射转换为字符串并拼接密钥String content = body + "." + secretKey;// 计算签名的摘要并返回摘要的十六进制表示形式return md5.digestHex(content);  }
}

服务端的校验:

java">@PostMapping("/user") 
public String getUserNameByPost(@RequestParam User user) {// 1.拿到五个参数后一步步校验// 从请求头中获取参数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");// 2.权限校验(实际应为到数据库中去查)if(!accessKey.equals("khr123")) {throw new RuntimeException("无权限"); }// 3.校验随机数if(Long.parseLong(nonce) > 10000) {throw new RuntimeException("无权限");}// 拼接 sign,实际 secretKey 应该从数据库中去查String serverSign = SignUtils.genSign(body, "1q2w3e4r"); // 如果生成的签名不一致,抛出异常,提示无权限if(!sign.equals(serverSign)) {throw new RuntimeException("无权限");} return "POST 用户名字是" + user.getUsername();}

签名认证中具体的参数应当根据业务场景灵活选择,可能会包含 userId 字段用来区分用户,也可能会增加 version 字段来表示应用程序版本号等。

三. 开发 SDK

1. 为什么需要 SDK

思考一个问题,上述 API 签名验证过程的确能够保证安全性,但如果要求开发者每次调用都要自己写一套签名算法未免太过繁琐。理想的情况是,开发者只需要关注调用哪些接口、传递哪些参数即可,就跟调用自己写的代码一样简单。

因此,需要开发一个简单易用的 SDK,给调用接口的开发者提供一个 starter,引入后,直接在 application.yml 中编写配置,即可自动创建客户端,更加方便开发者使用。类似于 spring boot 中的各种 starter,如引入 mybatis、redis、swagger 接口文档等。

<denpendency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version>
</denpendency>

引入 starter 后,可以直接在 application.yml 配置文件中编写相关配置,如 redis。

# redis 配置
redis:port: 6379host: localhostdatabase: 0

2. 开发流程

首先创建一个 spring boot 项目,并选择引入 Lombok、Spring Configuration Processor 依赖。Spring Configuration Processor 能够帮助开发者自动生成配置的代码提示。

然后进入 pom.xml 配置文件,删除一些不需要的依赖配置,比如测试类和 maven 构建项目的依赖(build 的内容)。

我们的目标是为用户生成一个可用的客户端对象,并且用户引入 starter 后能够直接使用,因此创建一个配置类 KApiClientConfig:

java">import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;// 通过 @Configuration 注解,将该类标记为一个配置类,告诉 Spring 这是一个用于配置的类
@Configuration
// 能够读取application.yml的配置,读取到配置之后,把这个读到的配置设置到我们这里的属性中,
// 这里给所有的配置加上前缀为"kapi.client"
@ConfigurationProperties("kapi.client")
// @Data 注解是一个 Lombok 注解,自动生成了类的getter、setter方法
@Data
// @ComponentScan 注解用于自动扫描组件,使得 Spring 能够自动注册相应的 Bean
@ComponentScan
public class KApiClientConfig {private String accessKey;private String secretKey;// 创建一个名为 kApiClient 的 Bean@Beanpublic KApiClient kApiClient() {// 使用 aK 和 sK 创建一个 KApiClient 实例return new KApiClient(accessKey, secretKey);}
}

将接口项目中的 client 包、model 包和 utils 包复制粘贴过来,注意添加 hutool 工具类的依赖,

给 KApiClient 类重新引入包,删除多余注解,完整代码如下:

java">import cn.hutool.core.util.RandomUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.khr.kapiclientsdk.model.User;import java.util.HashMap;
import java.util.Map;import static com.khr.kapiclientsdk.utils.SignUtils.genSign;/*** 调用第三方接口的客户端**/
public class KApiClient {private String accessKey;private String secretKey;public KApiClient(String accessKey, String secretKey) {this.accessKey = accessKey;this.secretKey = secretKey;}public String getNameByGet(String name) {// 可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中HashMap<String, Object> paramMap = new HashMap<>();paramMap.put("name", name);String result = HttpUtil.get("http://localhost:8123/api/name/", paramMap);System.out.println(result);return result;}public String getNameByPost(String name) {// 可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中HashMap<String, Object> paramMap = new HashMap<>();paramMap.put("name", name);String result= HttpUtil.post("http://localhost:8123/api/name/", paramMap);System.out.println(result);return result;}private Map<String, String> getHeaderMap(String body) {Map<String, String> hashMap = new HashMap<>();hashMap.put("accessKey", accessKey);// 一定不能直接发送// hashMap.put("secretKey", secretKey);hashMap.put("nonce", RandomUtil.randomNumbers(4));hashMap.put("body", body);hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));hashMap.put("sign", genSign(body, secretKey));return hashMap;}public String getUserNameByPost(User user) {String json = JSONUtil.toJsonStr(user);HttpResponse httpResponse = HttpRequest.post("http://localhost:8123/api/name/user").addHeaders(getHeaderMap(json)).body(json).execute();System.out.println(httpResponse.getStatus());String result = httpResponse.body();System.out.println(result);return result;}
}

最后,还需要创建一个 properties 文件用于指定要自动配置的类。在 resources 目录下创建 META-INF 目录,并在 META-INF 下新建一个 spring.factories 文件。

该文件定义了 Spring Boot 的自动配置,文件中每一行都是一个配置项,包含两个部分,= 前面是配置项的全限定类名,后面是对应的自动配置类。Spring Boot 应用启动时,会加载这个文件,并根据其中的配置项自动进行相应配置。

# spring boot starter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.khr.kapiclientsdk.KApiClientConfig

上述配置指定的要自动配置的类就是刚才编写的配置类 KApiClientConfig,当 Spring Boot 启动时会自动加载并实例化 KApiClientConfig,将其应用在程序中。这样就可以自动生成一个客户端对象,而无需手动创建,大大简化了开发者的调用过程。

将该 SDK 项目打包后,会保存在 maven 仓库中。

这样回到之前的接口项目,删除 client、model、utils 包和测试类,然后引入刚才写好的 SDK 依赖,在 application.yml 配置文件中编写配置,指定自己的 aK、sK,即可创建客户端:

kapi:client:access-key:khr123secret-key:1q2w3e4r

http://www.ppmy.cn/news/1473796.html

相关文章

Python入门 2024/7/6

目录 元组的定义和操作 字符串的定义和操作 字符串 字符串的替换 字符串的分割 字符串的规整操作&#xff08;去除前后空格&#xff09; 字符串的规整操作&#xff08;去掉前后指定字符串&#xff09; 操作 字符串的替换 字符串的分割 字符串的规整操作 统计字符串的…

前端面试题21(js排序方法)

JavaScript 中有多种内置和自定义的排序方法。内置的 .sort() 方法是最直接的排序方式&#xff0c;而自定义排序算法如冒泡排序、选择排序、插入排序、希尔排序、快速排序等则提供了更深层次的学习和应用价值。下面我将详细介绍这些排序方法&#xff0c;并给出相应的示例代码。…

Linux 常用指令详解

Linux 是一个强大而灵活的操作系统&#xff0c;掌握常用的 Linux 指令是使用和管理 Linux 系统的基础。本文将介绍一些常用的 Linux 指令&#xff0c;并附上 Vim 和 g 的常用指令说明&#xff0c;帮助你更好地进行开发和操作。 1. 基本文件操作指令 1.1 显示目录内容 ls常用…

存储结构与管理磁盘

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 目录 一、一切从“/”开始 二、物理设备的命名规则 三、文件系统与数据资料 四、挂载硬件设备 五、添加硬盘设备 六、添加交换分区 七、磁盘容…

如何从相机的存储卡中恢复原始照片

“不好了。” 当您意识到自己不小心从存储卡中删除了照片&#xff0c;或者错误地格式化了相机的记忆棒时&#xff0c;您首先会喊出这两个词。这是一种常见的情况&#xff0c;每个人一生中都会遇到这种情况。幸运的是&#xff0c;有办法从相机的 RAW 记忆棒中恢复已删除的照片。…

【云服务器介绍】选择指南 腾讯云 阿里云全配置对比 搭建web 个人开发 app 游戏服务器

​省流目录&#xff1a;适用于博客建站&#xff08;2-4G&#xff09;、个人开发/小型游戏[传奇/我的世界/饥荒]&#xff08;4-8G&#xff09;、数据分析/大型游戏[幻兽帕鲁/雾锁王国]服务器&#xff08;16-64G&#xff09; 1.京东云-专属活动 官方采购季专属活动地址&#xff1…

coco数据集格式计算mAP的python脚本

目录 背景说明COCOeval 计算mAPtxt文件转换为coco json 格式自定义数据集标注 背景说明 在完成YOLOv5模型移植&#xff0c;运行在板端后&#xff0c;通常需要衡量板端运行的mAP。 一般需要两个步骤 步骤一&#xff1a;在板端批量运行得到目标检测结果&#xff0c;可保存为yol…

【HICE】转发服务器实验

1.在本地主机上操作 2.在客户端操作设置主机的IP地址为dns 3.测试,客户机是否能ping通