对接第三方接口鉴权(Spring Boot+Aop+注解实现Api接口签名验证)

前言

一个web系统,从接口的使用范围也可以分为对内和对外两种,对内的接口主要限于一些我们内部系统的调用,多是通过内网进行调用,往往不用考虑太复杂的鉴权操作。但是,对于对外的接口,我们就不得不重视这个问题,外部接口没有做鉴权的操作就直接发布到互联网,而这不仅有暴露数据的风险,同时还有数据被篡改的风险,严重的甚至是影响到系统的正常运转
方案一:Spring Boot+Aop+注解实现Api接口签名验证
方案二:在已有接口上,拦截器拦截,接口路径,白名单匹配

Spring Boot+Aop+注解实现Api接口签名验证

appId和secret+token+时间戳
token的生成过程中在加入时间戳,校验token正确性之前先校验时间戳是否在一定时间窗口内(比如说1分钟),如果超过一分钟,直接拒绝请求,通过后再校验token。
新接口加上注解
由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。
一、为什么需要 API 接口签名
对外开放的 API 接口都会面临一些安全问题,例如伪装攻击、篡改攻击、重放攻击以及数据信息泄漏的风险。利用 API 接口签名能方便的防范这些安全问题和风险。在设计 API 接口签名时主要考虑以下几点:
请求发起时间得在限制范围内
请求的用户是否真实存在
是否存在重复请求
请求参数是否被篡改
1.保证请求数据正确
当请求中的某一个字段的值变化时,原有的签名结果就会发生变化。所以,只要参数发生变化,签名就要发生变化,否则请求将会是一个无效的请求。
2.保证请求来源合法
一般情况下,生成签名的算法都会成对出现一个 appKey 和一个 appSecret,根据 appKey 能识别出调用者身份;根据 appSecret 能识别出签名是否合法。
3.识别接口的时效性
一般情况下,签名和参数中会包含时间戳,这样服务端就可以验证客户端请求是否在有效时间内,从而避免接口被长时间的重复调用
4.是否存在重复请求

API 接口签名验签实现机制

签名验签流程图
在这里插入图片描述

1 客户端向服务端申请 appKey,appSecret ,服务端下发 appKey,appSecret。
2 客户端集成 SDK 产生 sign,将 appKey,请求参数,时间戳,sign,随机数nonce 发送到服务端,服务端根据请求参数使用 SDK 中的签名规则生成签名来验证sign的合法性,之后返回结果。

实现思路:

我们按照主要防御措施先后顺序来实现,首先已知我们得到以下四个参数:

// 供应商的id,验证用户的真实性
String appid = request.getHeader("appid");
// 请求发起的时间
String timestamp = request.getHeader("timestamp");
// 随机数
String nonce = request.getHeader("nonce");
// 签名算法生成的签名
String sign = request.getHeader("sign");1.请求发起时间得在限制范围内  
像这种比较简单,就是获取服务器的当前时间去跟请求发起时间比较。
// 限制为(含)60秒以内发送的请求
long time = 60;
long now = System.currentTimeMillis() / 1000;
if (now - Long.valueOf(timestamp) > time) {return ObjectResponse.fail("请求发起时间超过服务器限制时间");
}

2.请求的用户是否真实存在
一般会有以下两个场景
场景一:在前后端分离的模式中,用户登录后得到token,用户调用接口时传递token来确保用户的真实性。
场景二:接口调用方不需要登录,那么我们接口提供方可以提供appid(调用时需要传递)与secret(在签名算法中使用)给接口调用方来验证用户的真实性。
这里我主要说一下场景二,如下:
// 查询appid是否正确来验证用户的真实性

CoreApiKey apiKey = coreApiKeyService.selectByAppid(appid);
if (apiKey == null) {return ObjectResponse.fail("appid参数错误");
}
  1. 是否存在重复请求
    这里利用nonce参数,每次请求时先判断nonce在redis是否存在,存在则认为是重复请求,不存在就存放到redis中。但是这会有一个问题,随着请求的 次数越来越多,那么redis存放的nonce集合会越来越大,这肯定不是我们所期望的。这时我们可以巧妙的利用在请求发起时间得在限制范围内中的time(服务器限制60秒以内发生的请求),因为此步骤主要是验证请求是否重复,如果timestamp时间戳变了,那就不是重复请求了,所以我们可以在nonce存放到redis时给它设置一个过期时间(60秒),这样既保证了nonce的唯一性也不会发生nonce集合的无限大。
// 验证请求是否重复
if (redisService.hasKeyHashItem("third_party_key", apiKey.getAppid() + nonce)) {return ObjectResponse.fail("请不要发送重复的请求");
} else {// 如果nonce没有存在缓存中,则加入,并设置失效时间(秒)redisService.setHashItem("third_party_key", apiKey.getAppid() + nonce, nonce, time);
}
  1. 请求参数是否被篡改
    利用签名算法来生成签名。主要就是接口调用方的签名算法必须与接口提供方的签名算法一致。签名算法可以自己捣鼓捣鼓,我这里是先对key进行字典序排序,然后以url的参数格式进行拼接(secret在最后拼接),最后进行md5加密,以下一个Api接口签名验证就大功告成啦!
JSONObject signObj = new JSONObject();
signObj.put("appid", appid);
signObj.put("timestamp", timestamp);
signObj.put("nonce", nonce);
String mySign = getSign(signObj, apiKey.getSecret());
// 验证签名
if (!mySign.equals(sign)) {return ObjectResponse.fail("签名信息错误");
}/*** 获取签名信息* @param data* @param secret* @return*/
private static String getSign(JSONObject data, String secret) {// 由于map是无序的,这里主要是对key进行排序(字典序)Set<String> keySet = data.keySet();String[] keyArr = keySet.toArray(new String[keySet.size()]);Arrays.sort(keyArr);StringBuilder sbd = new StringBuilder();for (String k : keyArr) {if (StringUtil.isNotEmpty(data.getString(k))) {sbd.append(k + "=" + data.getString(k) + "&");}}// secret最后拼接sbd.append("secret=").append(secret);return MD5Util.encode(sbd.toString());
}5.基于SringBoot以及Redis使用Aop来实现Api接口签名验证的源码  
@Component
@Aspect
@Slf4j
public class ThridPartyApiAspect {@Autowiredprivate HttpServletRequest request;@Autowiredprivate HttpServletResponse response;@Autowiredprivate RedisService redisService;@Autowiredprivate CoreApiKeyService coreApiKeyService;/*** 表示匹配带有自定义注解的方法*/@Pointcut("@annotation(com.stan.framework.anno.ThridPartyApi)")public void pointcut() {}@Around("pointcut()")public Object around(ProceedingJoinPoint point) {try {// 供应商的id,验证用户的真实性String appid = request.getHeader("appid");// 请求发起的时间String timestamp = request.getHeader("timestamp");// 随机数String nonce = request.getHeader("nonce");// 签名算法生成的签名String sign = request.getHeader("sign");if (StringUtil.isEmpty(appid) || StringUtil.isEmpty(timestamp) || StringUtil.isEmpty(nonce) || StringUtil.isEmpty(sign)) {return ObjectResponse.fail("请求头参数不能为空");}// 限制为(含)60秒以内发送的请求long time = 60;long now = System.currentTimeMillis() / 1000;if (now - Long.valueOf(timestamp) > time) {return ObjectResponse.fail("请求发起时间超过服务器限制时间");}// 查询appid是否正确CoreApiKey apiKey = coreApiKeyService.selectByAppid(appid);if (apiKey == null) {return ObjectResponse.fail("appid参数错误");}// 验证请求是否重复if (redisService.hasKeyHashItem("third_party_key", apiKey.getAppid() + nonce)) {return ObjectResponse.fail("请不要发送重复的请求");} else {// 如果nonce没有存在缓存中,则加入,并设置失效时间(秒)redisService.setHashItem("third_party_key", apiKey.getAppid() + nonce, nonce, time);}JSONObject signObj = new JSONObject();signObj.put("appid", appid);signObj.put("timestamp", timestamp);signObj.put("nonce", nonce);String mySign = getSign(signObj, apiKey.getSecret());// 验证签名if (!mySign.equals(sign)) {return ObjectResponse.fail("签名信息错误");}try {return point.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}} catch (Exception e) {e.printStackTrace();return ObjectResponse.fail("解析请求参数异常");}return null;}/*** 获取签名信息* @param data* @param secret* @return*/private static String getSign(JSONObject data, String secret) {// 由于map是无序的,这里主要是对key进行排序(字典序)Set<String> keySet = data.keySet();String[] keyArr = keySet.toArray(new String[keySet.size()]);Arrays.sort(keyArr);StringBuilder sbd = new StringBuilder();for (String k : keyArr) {if (StringUtil.isNotEmpty(data.getString(k))) {sbd.append(k + "=" + data.getString(k) + "&");}}// secret最后拼接sbd.append("secret=").append(secret);return MD5Util.encode(sbd.toString());}
}6. 测试签名
/*** @Author lc* @description:* @Date 2022/4/26 15:19* @Version 1.0*/@RestController
@Api(tags = "对外接口")
@RequestMapping("/test")
public class ThirdPartyApiAspectController  {@ThirdPartyApi@ApiOperation("对接接口测试")@PostMapping(value = "/test")public ResponseVO<String> test(HttpServletRequest request){return new ResponseVO ("签名校验");}
}

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

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

相关文章

C语言scanf()函数详解

目录 1. scanf&#xff08;&#xff09;函数简介 1.1 函数原型 1.2 头文件 1.3 返回值 1.4 参数 2.格式说明符 3.输入格式控制 关于‘ * ’的例子 关于width域宽的例子 关于length长度修饰符的说明 4. 其他常见问题说明 4.1 scanf&#xff08;&#xff09;函数连…

深入理解计算机系统(1):开始

计算机系统是由硬件和系统软件组成的&#xff0c;它们共同工作来运行应用程序。虽然系统的具体实现方式随着时间不断变化&#xff0c;但是系统内在的概念却没有改变。所有计算机系统都有相似的硬件和软件组件&#xff0c;它们又执行着相似的功能。 计算机系统 信息就是位上下…

从零学Java - 接口

Java 接口 文章目录 Java 接口1.接口的语法1.1 与抽象类的区别 2.如何使用接口?2.1 接口的使用规范 3.什么是接口?3.1 常见关系 4.接口的多态性5.面向接口编程5.1 接口回调 6.特殊接口6.1 常量接口6.2 标记接口 7.接口的好处 补充面向对象 七大设计原则 1.接口的语法 接口&a…

1-02VS的安装与测试

一、概述 对于一名C语言程序员而言&#xff0c;进行C语言程序的开发一般需要一个文本编辑器加上一个编译器就足够了。但为了方便起见&#xff0c;我们选择使用集成开发环境——Visual Studio&#xff08;简称VS&#xff09;。安装Visual Studio 下面讲一下如何安装VS&#xff0…

深度学习中的知识蒸馏

一.概念 知识蒸馏&#xff08;Knowledge Distillation&#xff09;是一种深度学习中的模型压缩技术&#xff0c;旨在通过从一个教师模型&#xff08;teacher model&#xff09;向一个学生模型&#xff08;student model&#xff09;传递知识来减小模型的规模&#xff0c;同时保…

CSS基础笔记-03选择器

CSS基础笔记系列 《CSS基础笔记-01CSS概述》《CSS基础笔记-02动画》 前言 在前面两篇博客中&#xff0c;我实际上已经使用过了选择器。但到底什么是选择器、有什么作用&#xff0c;我反而不能表达出来。因此&#xff0c;决定记录了我的学习和思考。 什么是选择器 selector…

软件测试/测试开发丨Selenium 高级控件交互方法

一、使用场景 使用场景对应事件复制粘贴键盘事件拖动元素到某个位置鼠标事件鼠标悬停鼠标事件滚动到某个元素滚动事件使用触控笔点击触控笔事件&#xff08;了解即可&#xff09; www.selenium.dev/documentati… 二、ActionChains解析 实例化类ActionChains&#xff0c;参…

VLM,LLM等大模型如何应用于机器人控制(以强化学习为例)

VLM&#xff1a;视觉语义模型&#xff0c;准确识别图中有什么&#xff0c;处于什么状态&#xff0c;以及不同物体之间的关联。 LLM&#xff1a;语言大模型&#xff0c;可以针对当前的环境&#xff0c;自动生成可执行的任务&#xff0c;或者将人类指令重新分成可执行的子任务。…

MySQL之视图内连接、外连接、子查询

一、视图 1.1 含义 虚拟表&#xff0c;和普通表一样使用 视图&#xff08;view&#xff09;是一个虚拟表&#xff0c;其内容由查询定义。同真实的表一样&#xff0c;视图包含一系列带有名称的列和行数据。但是&#xff0c;数据库中只存放了视图的定义&#xff0c;而并没有存放…

函数图像化

函数图像化 在进行模型提取时&#xff0c;往往会需要选择拟合的函数&#xff0c;因此&#xff0c;了解函数的图像对于模型拟合提取有益&#xff0c;以下是常见的一些函数的曲线 1 二次函数 常见的耳二次函数曲线&#xff0c;转换x与y数量级差异仅一个数量级&#xff0c; 2 三…

使用EXCEL计算相关系数R和R方

对于EXCEL中的两列&#xff0c;一列是预测值&#xff0c;另一列是实测值 得到的R为0.73 R方就是R*R为0.49左右

element-ui table-自定义表格某列的表头样式或者功能

自带表格 自定义表格某列的表头样式或者功能 <el-table><el-table-column :prop"date">//自定义表身每行数据<template slot-scope"scope">{{scope.row[scope.column.label] - ? - : scope.row[scope.column.label]}}</template>…