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

news2024/12/23 17:56:22

一. 接口调用

对于开发者来说,接口的调用应当是方便快捷的,而且出于安全考虑,通常会选择在后端调用第三方 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);
       // 返回构造的请求头 map
       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())
             .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 算法的 Digester
     Digester md5 = new Digester(DigesterAlgorithm.MD5);
     // 构建签名内容,将哈希映射转换为字符串并拼接密钥
     String content = body + "." + secretKey;
     // 计算签名的摘要并返回摘要的十六进制表示形式
     return md5.digestHex(content);  
  }
}

后续会持续更新整理。 

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

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

相关文章

kaggle量化赛金牌方案(第七名解决方案)(下)

— 无特征工程的神经网络模型&#xff08;得分 5.34X&#xff09; 比赛进入最后阶段&#xff0c;现在是时候深入了解一些关于神经网络模型的见解了。由于 Kaggle 讨论区的需求&#xff0c;我在这里分享两个神经网络模型。第一个是 LSTM 模型&#xff0c;第二个是卷积网络&…

PyPDF2指定范围拆分PDF文件为单个页面

本文目录 前言一、指定范围拆分PDF1、过程讲解2、拆分效果图3、完整代码二、其他问题1、更改页码索引值前言 上一篇文章讲解了怎么讲一个PDF文档分割为多个单页面PDF,本文来讲解一下进阶,就是指定范围拆分PDF页面,有的时候,我们只想把PDF文档中的某几页拆分出来,而不是全…

【论文解读】iSDF: Real-Time Neural Signed Distance Fields for Robot Perception

《iSDF: Real-Time Neural Signed Distance Fields for Robot Perception》提出了一种用于实时签名距离场&#xff08;SDF&#xff09;重建的持续学习系统。 论文&#xff1a;https://arxiv.org/abs/2204.02296https://arxiv.org/abs/2204.02296 项目&#xff1a;iSDFhttps:/…

QT创建地理信息shp文件编辑器shp_editor

空闲之余创建一个简单的矢量shp文件编辑器&#xff0c;加深对shp文件的理解。 一、启动程序 二、打开shp文件 三、显示shp文件的几何图形 四、双击右边表格中的feature&#xff0c;主窗体显示选中feature的各个节点。 五、鼠标在主窗体中选中feature的节点&#xff0c;按鼠标左…

【坚果识别】果实识别+图像识别系统+Python+计算机课设+人工智能课设+卷积算法

一、介绍 坚果识别系统&#xff0c;使用Python语言进行开发&#xff0c;通过TensorFlow搭建卷积神经网络算法模型&#xff0c;对10种坚果果实&#xff08;‘杏仁’, ‘巴西坚果’, ‘腰果’, ‘椰子’, ‘榛子’, ‘夏威夷果’, ‘山核桃’, ‘松子’, ‘开心果’, ‘核桃’&a…

Python爬虫实战案例——王者荣耀皮肤抓取

大家好&#xff0c;我是你们的老朋友——南枫&#xff0c;今天我们一起来学习一下该如何抓取大家经常玩的游戏——王者荣耀里面的所有英雄的皮肤。 老规矩&#xff0c;直接上代码&#xff1a; 导入我们需要使用到的&#xff0c;也是唯一用到的库&#xff1a; 我们要抓取皮肤其…

使用ref定义响应式数据变量

Ref 使用 Ref 可以方便地创建和管理Vue组件中的响应式数据。例如&#xff0c;如果你有一个计数器组件&#xff0c;你可以使用 Ref 来创建一个响应式的计数器变量&#xff0c;然后在组件内部或外部修改这个变量的值&#xff0c;而不需要手动触发视图更新。 先声明一个变量&…

数据结构初阶 堆的问题详解(三)

题目一 4.一棵完全二叉树的节点数位为531个&#xff0c;那么这棵树的高度为&#xff08; &#xff09; A 11 B 10 C 8 D 12 我们有最大的节点如下 假设最大高度为10 那么它的最多节点应该是有1023 假设最大高度为9 那么它的最多节点应该是 511 所以说这一题选B 题目二 …

昇思25天学习打卡营第11天|基于MindSpore通过GPT实现情感分类

学AI还能赢奖品&#xff1f;每天30分钟&#xff0c;25天打通AI任督二脉 (qq.com) 基于MindSpore通过GPT实现情感分类 %%capture captured_output # 实验环境已经预装了mindspore2.2.14&#xff0c;如需更换mindspore版本&#xff0c;可更改下面mindspore的版本号 !pip uninsta…

【深海王国】小学生都能玩的语音模块?ASRPRO打造你的第一个智能语音助手(4)

Hi~ (o^^o)♪, 各位深海王国的同志们&#xff0c;早上下午晚上凌晨好呀~ 辛勤工作的你今天也辛苦啦(/≧ω) 今天大都督继续为大家带来系列——小学生都能玩的语音模块&#xff0c;帮你一周内快速学会语音模块的使用方式&#xff0c;打造一个可用于智能家居、物联网领域的语音助…

01 Docker 概述

目录 1.Docker简介 2.传统虚拟机 vs 容器 3.Docker运行速度快的原因 4.Docker基本组成三要素 5.Docker 平台架构 入门版 架构版 1.Docker简介 Docker是基于Go语言实现的云开源项目。 Docker的主要目标是&#xff1a;Build, Ship and Run Any App, Anywhere&#xff0c…

抖音常用的视频剪辑软件有哪些,变速视频如何制作?

抖音是一款当下流行的短视频软件。很多人都想在上面发表自己的作品&#xff0c;但是也还有人因为不会剪辑&#xff0c;找不到合适的视频制作软件&#xff0c;一直没能行动。今天就为大家解答抖音常用的制作视频软件有哪些&#xff0c;如何调整抖音制作视频的速度。 希望大家看完…

AzureDataFactory 实体间的关联如何处理(Lookup)

使用ADF从外部数据源(例如Sql Server)往D365推数时&#xff0c;实体间的Lookup一定是要做的&#xff0c;本篇以我项目中的设备为例&#xff0c;设备表中有产品的lookup字段 设备表结构如下 msdyn_customerasset 表名ID 设备表guidSerialNumber设备序列号ProductCode设备对应的…

Hadoop3:NameNode和DataNode多目录配置(扩充磁盘的技术支持)

一、NameNode多目录 1、说明 NameNode多目录&#xff0c;需要在刚搭建Hadoop集群的时候&#xff0c;就配置好 因为&#xff0c;配置这个&#xff0c;需要格式化NameNode 所以&#xff0c;如果一开始没配置NameNode多目录&#xff0c;后面&#xff0c;就不要配置了。 2、配置…

Linux环境下的字节对齐现象

在Linux环境下&#xff0c;字节对齐是指数据在内存中的存储方式。字节对齐是为了提高内存访问的效率和性能。 在Linux中&#xff0c;默认情况下&#xff0c;结构体和数组的成员会进行字节对齐。具体的对齐方式可以通过编译器选项来控制。 在使用C语言编写程序时&#xff0c;可…

技术市集 | 如何通过WSL 2在Windows上挂载Linux磁盘?

你是否常常苦恼&#xff0c;为了传输或者共享不同系统的文件需要频繁地在 Windows 和 Linux 系统之间切换&#xff0c;既耽误工作效率&#xff0c;也容易出错。 那么有没有一种办法&#xff0c;能够让你在Windows系统中像访问本地硬盘一样来操作Linux系统中的文件呢&#xff1…

jni原理和实现

一、jni原理 主要就是通过数据类型签名和反射来实现java与c/c方法进行交互的 数据类型签名对应表 javac/cbooleanZbyteBcharCshortSintIlongLfloatFdoubleDvoidVobjectL开头&#xff0c;然后以/分割包的完整类型&#xff0c;后面再加; 比如String的签名就是Ljava/long/Strin…

基于jeecgboot-vue3的Flowable流程-集成仿钉钉流程(一)一些样式的调整使用

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 1、比如下面的发起人双击后出现的界面不正常&#xff0c; 看它的样式主要是这个里面的margin-left应该太小了&#xff0c; [data-v-45b533d5] .el-tabs__content { margin-top: 50px;mar…

实用麦克风话筒音频放大器电路设计和电路图

设计目标 输入电压最大值输出电压最大值电源Vcc电源Vee频率响应偏差20Hz频率响应偏差20kHz100dB SPL(2Pa)1.228Vrms5V0V–0.5dB–0.1dB 设计说明 此电路使用跨阻抗放大器配置中的运算放大器将驻极体炭精盒麦克风的输出电流转换为输出电压。此电路的共模电压是固定的&#xf…

第15届蓝桥杯Python青少组选拔赛(STEMA)2023年8月真题-附答案

第15届蓝桥杯Python青少组选拔赛&#xff08;STEMA&#xff09;2023年8月真题 题目总数&#xff1a; 11 总分数&#xff1a; 400 一、单选题 第 1 题 单选题 以下不符合 Python 语言变量命名规则的是&#xff08; &#xff09;。 A. k B. 2_k C. _k D. ok 答案 B …