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

一. 接口调用

对于开发者来说,接口的调用应当是方便快捷的,而且出于安全考虑,通常会选择在后端调用第三方 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>

三个接口:

/**** 查询用户名称的 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("/") public String getUserNameByPost(@RequestParam User user) {return "POST 用户名字是" + user.getUsername();}
}

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

/****调用第三方接口的客户端**/
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 的人都可以调用接口,不安全。

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

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

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

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 存在最大长度限制,如果传递的其它参数过多,会导致关键数据被挤出,所以建议从请求头中获取这些数据。

@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 是否被分配过等,此处仅作模拟。

/****调用第三方接口的客户端**/
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 随意修改为其它值,则会提示无权限:

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

5. 安全传递

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

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

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

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

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

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

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

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

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

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

6. 安全传递实现

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

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));return hashMap;
}

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

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

/*** 签名工具*/
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);  }
}

后续会持续更新整理。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/865494.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

视图库对接系列(GA-T 1400)四、视图库对接系列(本级)注册

视图库对接系列(本级)注册 在之前的步骤中&#xff0c;我们已经把项目大体的架构已经写出来了。那我们就来实现注册接口。 GA-T 1400中的步骤如下&#xff1a; 这里的话&#xff0c;我们实现的简单点&#xff0c; 我们不进去鉴权&#xff0c;也就是设备或平台找我们注册的话&…

Python高速下载及安装的十大必备事项与C++联调

选择正确的版本&#xff1a; 访问Python官网&#xff08;https://www.python.org/&#xff09;下载最新稳定版本&#xff0c;目前最新稳定版本为3.12.4 避免下载并安装Python 2.x版本&#xff0c;因为它已经停止维护。 选择适合操作系统的安装包&#xff1a; 根据你的操作系…

【笔记】太久不用redis忘记怎么后台登陆了

&#xff01;首先启动虚拟机linux的centos7 2.启动finalshell 我的redis启动在根目录用 redis-server redis.conf --启动 systemctl status redis --查看redis状态 是否active redis-cli -h centos的ip地址 -p 你要用的redis端口号&#xff08;默认为6379&#xff09; -a 你…

uboo对内存操作读写命令的基本使用

内存操作命令 直接对DRAM进行读写的操作,uboot常用的内存操作命令有md,nm,mm,mw,cp和cmp. md命令: 显示内存值 # md md - memory display Usage: md [.b, .w, .l, .q] address [# of objects] b:1个字节 byte w:2个字节 world l:4个字节 long of objects 以word 为单位的1…

MySQL:保护数据库

保护数据库 1. 用户1.1 创建用户1.2 查看用户1.3 删除用户1.4 修改密码 2. 权限2.1 授予权限2.2 查看权限2.3 撤销权限 之前都是介绍本地数据库而你自己就是数据库的唯一用户&#xff0c;所以不必考虑安全问题。但实际业务中数据库大多放在服务器里&#xff0c;你必须妥善处理好…

版本控制系统:Git 纯应用(持续更新);

基本操作 ctrl上行键&#xff1a;上次代码 本地仓库&#xff1a;Git init 新建文件&#xff1a;touch xxxx.xxx 查看状态&#xff1a;Git status 文件从工作区——暂存区&#xff1a;Git add ./文件名(.是通配符代表所有) 暂存区——仓库&#xff1a;Git commit -m &…

论文阅读之旋转目标检测ARC:《Adaptive Rotated Convolution for Rotated Object Detection》

论文link&#xff1a;link code&#xff1a;code ARC是一个改进的backbone&#xff0c;相比于ResNet&#xff0c;最后的几层有一些改变。 Introduction ARC自适应地旋转以调整每个输入的条件参数&#xff0c;其中旋转角度由路由函数以数据相关的方式预测。此外&#xff0c;还采…

使用 Ollama 时遇到的问题

题意&#xff1a; ImportError: cannot import name Ollama from llama_index.llms (unknown location) - installing dependencies does not solve the problem Python 无法从 llama_index.llms 模块中导入名为 Ollama 的类或函数 问题背景&#xff1a; I want to learn LL…

【postgreessql 】统计库中的所有表数量

在PostgreSQL中&#xff0c;你可以使用SQL查询来统计数据库中的所有表数量。这通常涉及到查询系统目录表&#xff0c;特别是 pg_catalog.pg_tables 表&#xff0c;它存储了关于数据库中所有表的信息。 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema IN …

mysql逗号分割字符串“1,2,3”实现in查询

数据示例 前台单值参数实现in查询 主要函数FIND_IN_SET 该函数的作用是查询字段(strlist) 中是否包含(str)的结果&#xff0c;返回结果为 null或记录 select id,recommend_position_id from t_stk_task where FIND_IN_SET(359919,recommend_position_id)查询效果 前台集…

【软件测试】性能测试 | 概念 | 常见术语 | 性能指标 | 分类 | 流程

性能测试 文章目录 性能测试一、什么是性能测试1.生活中遇到的软件的性能问题2.性能测试定义3.性能测试和功能测试有什么区别4.什么样的表现属于软件性能好、什么样的表现不好5.影响一个软件性能因素有哪些 二、为什么进行性能测试三、性能测试的常见术语以及性能测试的衡量指标…

C++部分复习笔记下

7. C11 范围for 使用格式 vector<int> v { 1,2,3,4,5 }; for (auto e : v) {cout << e << " "; } cout << endl;底层原理&#xff0c;使用迭代器 vector<int> v { 1,2,3,4,5 }; auto it v.begin(); while (it ! v.end()) {cout…

如何在TechNow招聘顶尖AI工程师

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Spring Boot 高级配置:如何轻松定义和读取自定义配置

目录 1. 环境准备 2. 读取配置数据 2.1 使用 Value注解 2.2 Environment对象 2.3.2.3 自定义对象 这篇博客我们将深入探讨如何在Spring Boot应用中有效地定义和读取自定义配置。掌握这一技巧对于任何希望优化和维护其应用配置的开发者来说都是至关重要的。我们将从基础开始…

企业消费采购成本和员工体验如何实现“鱼和熊掌“的兼得?

有企业说企业消费采购成本和员工体验的关系好比是“鱼和熊掌”&#xff0c;无法兼得&#xff1f; 要想控制好成本就一定要加强管控&#xff0c;但是加强管控以后&#xff0c;就会很难让员工获得满意的体验度。如果不加以管控&#xff0c;员工自由度增加了&#xff0c;往往就很难…

动态住宅代理IP的3个优点

在大数据时代的背景下&#xff0c;代理IP成为了很多企业顺利开展的重要工具。代理IP地址可以分为住宅代理IP地址和数据中心代理IP地址。选择住宅代理IP的好处是可以实现真正的高匿名性&#xff0c;而使用数据中心代理IP可能会暴露自己使用代理的情况。 住宅代理IP是指互联网服务…

Jenkins教程-13-参数化任务构建

上一小节我们学习了发送html邮件测试报告的方法&#xff0c;本小节我们讲解一下Jenkins参数化任务构建的方法。 很多时候我们需要根据不同的条件去执行构建&#xff0c;如自动化测试中执行test、stg、prod环境的构建&#xff0c;Jenkins是支持参数化构建的。 以下是Jenkins官…

【C语言】手撕结构体内存对齐

©作者:末央&#xff06; ©系列:C语言初阶(适合小白入门) ©说明:以凡人之笔墨&#xff0c;书写未来之大梦 目录 结构体对齐规则结构体大小计算 - 三步曲 结构体对齐规则 怎么计算结构体的内存大小。这就涉及到结构体内存对齐的问题。 结构体的第⼀个成员对⻬到…

【C++】多态详解

&#x1f497;个人主页&#x1f497; ⭐个人专栏——C学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 目录 一、多态概念 二、多态的定义及实现 1. 多态的构成条件 2. 虚函数 2.1 什么是虚函数 2.2 虚函数的重写 2.3 虚函数重写的两个…

PyPDF2拆分PDF文件为单个页面

本文目录 前言一、拆分成为单页1、代码解析2、处理效果图3、完整代码二、其它知识1、enumerate是什么① 语法② 功能③ 示例④ 更改起始索引前言 感觉之前写的不是很满意,为了充分满足付费用户的权益,所以打算把PyPDF2的各种类和用法都重新写一下,以便满足所有订阅用户的各…