API项目3:API签名认证

news2024/10/22 9:47:25

问题引入

我们为开发者提供了接口,却对调用者一无所知

假设我们的服务器只能允许 100 个人同时调用接口。如果有攻击者疯狂地请求这个接口,那是很危险的。一方面这可能会损害安全性,另一方面耗尽服务器性能,影响正常用户的使用。

因此我们必须为接口设置保护措施,例如限制每个用户每秒只能调用十次接口,即实施请求频次的限额控制。所以我们必须知道谁在调用接口,并且不能让无权限的人随意调用。

在我们之前开发后端时,我们会进行一些权限检查。例如,当管理员执行删除操作时,后端需要检查这个用户是否为管理员,直接从后端的 session 中获取的。但问题来了,比如我是前端直接发起请求,没有登录操作,没有输入用户名和密码,我怎么去调用呢?

API 签名认证

API 签名认证过程

签发签名  -> 使用签名或校验签名

为什么需要API签名认证

为了保证安全性,不能让任何人都能调用接口。

适用于无需保存登录态的场景。只认签名,不关注用户登录态(为了更通用)。

如何在后端实现签名认证?

需要两个东西,即 accessKeysecretKey,来标识用户

和用户名和密码类似,不过每次调用接口都需要带上,实现无状态的请求。

“无状态”指的是每个请求都是独立的,服务器不会保存客户端的任何状态信息。每次请求都包含所有必要的信息来完成该请求。这种设计使得系统更易于扩展和管理。

签发 accessKey 和 secretKey

一般来说,accessKey 和 secretKey 需要尽可能复杂无规律,防止黑客尝试破解,特别是密码。

签名认证实现

通过 http request header 头传递参数。

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

加密方式:

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

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

怎么知道这个签名对不对?

服务端用一模一样的参数和算法去生成签名,只要和用户传的的一致,就表示一致。

怎么防重放?

  • 参数 5:加 nonce 随机数,只能用一次,(存在问题:服务端要保存使用过的随机数)

所以配合

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

API 签名认证是一个很灵活的设计,具体要有哪些参数、参数名如何一定要根据场景来。

为什么需要两个 key?

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

TODO:关于 accessKey、secretKey 的生成方法,自行编写代码实现

实践:

在客户端 拿到 accessKey、secretKey

需要获取用户传递的 accessKey 和 secretKey。

对于这种数据,建议不要直接在 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 不等于 "sujie" 或者 secretKey 不等于 "abcdefgh"
    if (!accessKey.equals("sujie") || !secretKey.equals("abcdefgh")){
        // 抛出一个运行时异常,表示权限不足
        throw new RuntimeException("无权限");
    }
    // 如果权限校验通过,返回 "POST 用户名字是" + 用户名
    return "POST 用户名字是" + user.getUsername();
}

改造一下 SuApiClient.java,发请求可以带上 header,用这个就可以去添加很多的请求头

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

    // 创建一个私有方法,用于构造请求头
    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;
    }

安全传递

存在的问题

我们的请求有可能被人拦截,我们将密码放在请求头中,如果有中间人拦截到了你的请求,他们就可以直接从请求头中获取你的密码,然后使用你的密码发送请求。

密码绝对不能传递。也就是说,在向对方发送请求时,密码绝对不能以明文的方式传递,必须通过特殊的方式进行传递。

我们需要对该密码进行加密,这里通常称之为签名。 

可以将用户传递的参数与该密钥拼接在一起,然后使用单向签名算法进行加密。

如何防止重放请求有两种方式可以考虑:

第一种方式是通过加入一个随机数实现标准的签名认证。每次请求时,发送一个随机数给后端。后端只接受并认可该随机数一次,一旦随机数被使用过,后端将不再接受相同的随机数。这种方式解决了请求重放的问题,因为即使对方使用之前的时间和随机数进行请求,后端会认识到该请求已经被处理过,不会再次处理。然而,这种方法需要后端额外开发来保存已使用的随机数。并且,如果接口的并发量很大,每次请求都需要一个随机数,那么可能会面临处理百万、千万甚至亿级别请求的情况。因此,除了使用随机数之外,我们还需要其他机制来定期清理已使用的随机数。

第二种方式是加入一个时间戳(timestamp)。每个请求在发送时携带一个时间戳,并且后端会验证该时间戳是否在指定的时间范围内,例如不超过10分钟或5分钟。这可以防止对方使用昨天的请求在今天进行重放。通过这种方式,我们可以一定程度上控制随机数的过期时间。因为后端需要同时验证这两个参数,只要时间戳过期或随机数被使用过,后端会拒绝该请求。因此,时间戳可以在一定程度上减轻后端保存随机数的负担。通常情况下,这两种方法可以相互配合使用。

因此,在标准的签名认证算法中,建议至少添加以下五个参数:accessKey、secretKey、sign、nonce(随机数)、timestamp(时间戳)。此外,建议将用户请求的其他参数,例如接口中的 name 参数,也添加到签名中,以增加安全性。

安全传递实现

新建一个 utils 包,在 utils 包下新建 SignUtils.java(签名工具)

这个 hashmap 还需要进行拼接,我们传递的是用户的这些参数,但其实没有必要传递那么多参数,直接将 body 作为参数传递进来(在这里,我们也可以传递 hashmap,只要有一些共同的参数,能让客户端和服务端之间保持一致即可)。

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

刚刚客户端只有这两个参数 accessKey、secretKey

现在再加几个参数

/**
 * 获取请求头的哈希映射
 * @param body 请求体内容
 * @return 包含请求头参数的哈希映射
 */
private Map<String, String> getHeaderMap(String body) {
    Map<String, String> hashMap = new HashMap<>();
    hashMap.put("accessKey", accessKey);
    // 注意:不能直接发送密钥
    // hashMap.put("secretKey", secretKey);
	// 生成随机数(生成一个包含100个随机数字的字符串)
    hashMap.put("nonce", RandomUtil.randomNumbers(4));
	// 请求体内容
    hashMap.put("body", body);
	// 当前时间戳
	// System.currentTimeMillis()返回当前时间的毫秒数。通过除以1000,可以将毫秒数转换为秒数,以得到当前时间戳的秒级表示
	// String.valueOf()方法用于将数值转换为字符串。在这里,将计算得到的时间戳(以秒为单位)转换为字符串
    hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
	// 生成签名
    hashMap.put("sign", genSign(body, secretKey));
    return hashMap;
}

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

接下来服务端

        @PostMapping("/user")
        public String getUserNameByPost(@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");
        // 不能直接获取秘钥
        // String secretKey = request.getHeader("secretKey");

        // TODO 2.校验权限,这里模拟一下,直接判断 accessKey 是否为"yupi",实际应该查询数据库验证权限
        if (!accessKey.equals("sujie")){
            throw new RuntimeException("无权限");
        }

        // TODO 3.校验一下随机数,因为时间有限,就不带大家再到后端去存储了,后端存储用hashmap或redis都可以
        // 校验随机数,模拟一下,直接判断nonce是否大于10000
        if (Long.parseLong(nonce) > 10000) {
            throw new RuntimeException("无权限");
        }

        // TODO 4.校验时间戳与当前时间的差距,交给大家自己实现
        //
        //   if (timestamp) {}

        // TODO 5. 从实际数据库中取得用户secretKey
        String serverSign = SignUtils.genSign(body, "abcdefgh");
        if (!serverSign.equals(sign)) {
            throw new RuntimeException("无权限");
        }

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

整个签名认证算法的流程就是这样

需要强调的是,API签名认证是一种非常灵活的设计,具体需要哪些参数以及参数名的选择都应根据具体场景来确定。尽量避免在前端进行签名认证,而是由服务端来处理 

例如,某些公司或项目的签名认证可能会包含 userId 字段以区分用户。还可能包含 appId 和 version 字段来表示应用程序的版本号。有时还会添加一些固定的盐值等等。

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

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

相关文章

Linux命令——ls

命令格式&#xff1a;命令本身选项命令的指向目标 1.ls命令作用为列出目录下的内容 #lls后的选项有[-a,-l,-h]##注意ls与选项间应用空格隔开. 如下图为&#xff08;ls命令体-l选项/根文件&#xff09;的命令行 # ls -a 为&#xff1a;列出所有文件&#xff08;包括隐藏文件&…

解析:ARM 工业计算机在光伏储能中的关键作用

在当今能源转型的大背景下&#xff0c;光伏储能作为一种可持续、高效的能源解决方案&#xff0c;正受到越来越广泛的关注。而在光伏储能系统中&#xff0c;ARM 工业计算机以其卓越的性能和特点&#xff0c;成为了理想的选择。 一、光伏储能的重要性与挑战 全球对清洁能源的需…

视图使用控制器模板分配变量

文章目录 控制器视图路由配置控制器视图 视图使用控制器模板分配变量控制器视图 控制器视图 路由配置 <?phpuse Illuminate\Support\Facades\Route;/* |-------------------------------------------------------------------------- | Web Routes |---------------------…

服务性能优化之mybatis-plus 开启与关闭 SQL 日志打印

Hello&#xff01;欢迎各位新老朋友来看小弟博客&#xff0c;祝大家事业顺利&#xff0c;财源广进&#xff01;&#xff01; 主题&#xff1a;mybatis-plus 开启与关闭 SQL 日志打印 第一&#xff1a;开启打印 Mybatis-plus 需要通过下面的方式开启控制台 SQL 日志打印 myba…

2024 Mathorcup高校数学建模挑战赛ABCD题和 LaTeX 模版

01 A题-移动通信网络中 PCI 规划问题 02 B题-甲骨文智能识别中原始拓片单字自动分割与识别研究 03 C题-物流网络分拣中心货量预测及人员排班 &#xff08;左右滑动查看完整赛题&#xff09; 04 D题-量子计算在矿山设备配置及运营中的建模应用 看完了赛题&#xff0c;同学们…

同三维T80003JEH 4K30 HDMI解码器

1路HDMI输出&#xff0c; 1路3.5音频输出&#xff0c; 1个USB2.0,带1个RS232串口&#xff0c;1个网口&#xff0c;支持1路4K或4路1080P或9路720P及以下分辨率同时实时解码&#xff1b;支持视频画面输出1-16分割显示 同三维T80003JEH 4K30 HDMI解码器 同三维T80003JEH是一款4K3…

低成本轻量化5G网络部署redcap技术

RedCap&#xff08;Reduced Capability&#xff09;轻量化5G路由器旨在提供低功耗、成本效益高、性能较5G完整版稍微降低的解决方案。用于满足工业物联网&#xff08;IoT&#xff09;、消费电子产品和轻量级5G设备的需求。通过对5G技术进行一定程度的“功能裁剪”&#xff0c;降…

美食抖音视频素材网站推荐

为美食类抖音视频寻找高质量的素材&#xff0c;不仅能让作品更加吸引人&#xff0c;还能帮助创作者展现出精美的烹饪过程和独特的美食文化。以下推荐的素材网站&#xff0c;提供多种美食视频资源&#xff0c;从食材准备到烹饪技巧&#xff0c;甚至精致摆盘&#xff0c;让你的视…

postgres 的使用

postgres的常用命令&#xff1a; 查看所有库&#xff1a; \l 进入库 &#xff1a;\c 查看所有表&#xff1a;\d 库名; 查看表结构&#xff1a;\d 表名; 查看所有用户&#xff1a;\du 显示当前库下schema信息&#xff1a; \dn postgres的防火墙配置&#xff1a; 在安装目录的/va…

【升华】人生苦短,我要学python

一、python进阶成熟度阶梯 二、python进阶路线 三、python基础 Python 是由 Guido van Rossum 在八十年代末和九十年代初&#xff0c;在荷兰国家数学和计算机科学研究所设计出来的。 Python 本身也是由诸多其他语言发展而来的,这包括 ABC、Modula-3、C、C、Algol-68、SmallTa…

解决使用MobaXterm不能向Ubuntu上传下载文件的问题

如上图所示 解决方案 新建连接&#xff0c;使用root账户建立ssh会话&#xff0c;就是建立会话的时候&#xff0c;用户名使用root。ubuntu系统默认不允许远程root账户建立连接&#xff0c;表现就是你新建ssh会话&#xff0c;在第一步输入root密码的时候&#xff0c;密码正确会报…

ACM会议模板左上角论文标题太长导致重叠 解决方法

解决方法&#xff1a; 打开acmart.cls文件 搜索 \else % Proceedings\fancyfoot[C]{\ifACMprintfolios\footnotesize\thepage\fi}%\fancyhead[LO]{\ACMlinecountL\headfootfont\shorttitle}%fancyhead[LO]{…} 里定义了左上角的内容&#xff08;LO 表示 “Left Odd”&#xff0…

四.python核心语法2

目录 1.元组&#xff08;tuple&#xff09; 1.1. 创建元组和删除元组 1.2. 删除元组 1.3. 访问元组元素 1.4. 元组元素的修改 1) 重新赋值法 2) 类型转换法 1.5. 总结 2.字典{dictionary} 2.1. 创建 2.2. 字典元素的访问 2.3. 是否存在 2.4. 添加元素 2.5. 删除元素…

部署带证书的docker高可用的私有仓库harbor

一、部署带证书的docker高可用的私有仓库harbor harbor下载包&#xff1a;https://hub.fastgit.org/goharbor/harbor/releases docker阿里云地址&#xff1a;docker-ce-linux-centos-7-x86_64-stable-Packages安装包下载_开源镜像站-阿里云 Harbor 是 VMware 公司开源的企业级 …

刷题训练之多源 BFS

> 作者&#xff1a;დ旧言~ > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;熟练掌握多源 BFS算法。 > 毒鸡汤&#xff1a;学习&#xff0c;学习&#xff0c;再学习 ! 学&#xff0c;然后知不足。 > 专栏选自&#xff1a;刷…

quic-go源码一---server启动

前言&#xff1a; 走马观花地看了RFC 9000:QUIC: A UDP-Based Multiplexed and Secure Transport&#xff0c; 感受不是那么直观&#xff0c;所以再来看看这个协议的golang语言实现&#xff1a;quic-go,加强学习。 https://quic-go.net/docs/quic/quic-go文档 本篇准备的代…

threejs-法线向量

一、介绍 1.介绍 1.在3D计算机图形中&#xff0c;‘法向量’是一个向量&#xff0c;表示3d模型表面在某一点的方向。在每个顶点上&#xff0c;都会有一个关联的法向量&#xff0c;这个向量通常被归一化,也就是说它的长度为1。 2.使用:定点的法向属性在很多计算图形的领域都有应…

lammps统计一个原子周围不同类型原子数量的方法

本文介绍lammps统计一个原子周围不同类型原子数量的方法。 在之前的专栏中,曾介绍过动态统计某一个固定区域内原子数量的方法,也介绍过动态统计某一个原子周围原子数量的方法: 下面介绍第三种类型:动态统计某一个原子周围不同类型原子数量的方法。 以小球的随机碰撞为例,原…

【2024.10.14练习】生命之树

题目描述 题目分析 对于求树的子区域最大和&#xff0c;考虑使用树形DP求解。 假设以树的某一结点为根节点来深度优先搜索整棵树&#xff0c;搜索到每个结点时都会有两种决策状态&#xff1a;加入该节点和不加入该节点。定义代表选择此结点所能得到最大权值和&#xff0c;代表…

【Linux】解析信号的本质&相关函数及指令的介绍

前言 大家好吖&#xff0c;欢迎来到 YY 滴Linux系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…