对接第三方接口鉴权(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 {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HttpServletResponse response;

    @Autowired
    private RedisService redisService;

    @Autowired
    private 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 ("签名校验");
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java字符串对象池的作用是什么?

Java字符串对象池的作用是什么&#xff1f; 在 Java 中&#xff0c;字符串池&#xff08;String Pool&#xff09;是字符串常量的存储区域&#xff0c;它位于堆区域中。字符串池的作用是提高字符串的重用性&#xff0c;减少内存消耗。 字符串池的位置&#xff1a; 在堆中&…

Qt6入门教程 3:创建Hello World项目

一.新建一个项目 程序员的职业生涯都是从一声问候开始的&#xff0c;我们的第一个Qt项目也是HelloWorld 首先要说明的是&#xff0c;IDE不一定要用Qt Creator&#xff0c;用Visual Studio、VSCode、CLion也可以搭建Qt开发环境&#xff0c;它们都相应的插件来支持Qt开发。当然这…

气动凝结水回收机组 浮球机械泵回收机组工作原理动画讲解介绍

​ 1&#xff1a;气动凝结水回收浮球机械泵介绍 气动凝结水回收是一种利用气动力转换产生负压的装置&#xff0c;可以将废气中的水分分离出来并回收利用。这种装置主要包含两个关键部件&#xff1a;气水分离器和气动运动控制阀。 气水分离器负责将进入回收装置的废气中的水分…

transforms图像增强(二)

一、图像变换 1、transforms.Pad transforms.Pad是一个用于对图像边缘进行填充的数据转换操作。 参数&#xff1a; padding&#xff1a;设置填充大小。可以是单个整数&#xff0c;表示在上下左右四个方向上均填充相同数量的像素&#xff1b;也可以是一个包含两个整数的元组…

粉丝投稿:从写下第1个脚本到年薪20W,我的自动化测试心路历程

我希望我的故事能够激励现在的软件测试人&#xff0c;尤其是还坚持在做“点点点”的测试人。 你可能会有疑问&#xff1a;“我也能做到这一点的可能性有多大&#xff1f;”因此&#xff0c;我会尽量把自己做决定和思考的过程讲得更具体一些&#xff0c;并尽量体现更多细节。 每…

印象笔记02: 笔记本管理系统和空间使用

印象笔记02&#xff1a; 笔记本管理系统和空间使用 印象笔记新建笔记是一件非常容易的事情。笔记多了&#xff0c;就是归纳到笔记本里。 印象笔记一共有三层的笔记结构&#xff1a;最高层级是笔记本组&#xff0c;其次是笔记本&#xff0c;最后是一个个的笔记。合理的分类能够…

SpringCloud系列篇:核心组件之注册中心组件

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于SpringCloud的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.注册中心组件是什么 二.注册中心…

一键转换,创新无限:将HTML轻松转化为PDF!

在数字时代&#xff0c;HTML与PDF已成为信息传递的两大主流格式。然而&#xff0c;在这两者之间转换常常让人感到困扰。现在&#xff0c;有了我们的创新工具&#xff0c;您只需轻点一下&#xff0c;即可一键将HTML转化为PDF&#xff01; 首先&#xff0c;我们要进入首助编辑高…

(Python + Selenium4)Web自动化测试自学Day2之动手尝试

目录 文章声明⭐⭐⭐让我们开始今天的学习吧&#xff01;小试牛刀关于select标签关于弹窗只有一个点击按钮的弹窗需要确认的弹窗用户可以输入的弹窗 文章声明⭐⭐⭐ 该文章为我&#xff08;有编程语言基础&#xff0c;非编程小白&#xff09;的 Python Selenium4 Web自动化测试…

HttpRunner自动化测试工具之获取响应数据extract提取值到变量

获取响应数据 extract: 提取 注&#xff1a;extract 应与request保持同一层级 1、响应行&#xff0c;响应头&#xff1b;通过 extract 提取响应的数据并存储到变量中&#xff0c;如下图&#xff1a; 注&#xff1a;变量名的前面要有 - # 获取响应数据: 响应行&#xff08;…

【年终总结系列 2023】成长与收获:回顾过去、展望未来,加油2024!

转眼间加入CSDN已经六年多了&#xff0c;初加入CSDN时&#xff0c;我兴致勃勃地投入到写作中&#xff0c;分享了一些CTF的解题思路和方法&#xff0c;取得了不错的反响。但随着工作忙碌和生活压力的增加&#xff0c;我在CTF方面的写作频率逐渐减少&#xff0c;也很长时间没有更…

Linux离线安装MySQL(rpm)

目录 下载安装包安装MySQL检测安装结果服务启停MySQL用户设置 下载安装包 下载地址&#xff1a;https://downloads.mysql.com/archives/community/ 下载全量包如&#xff1a;(mysql-8.1.0-1.el7.x86_64.rpm-bundle.tar) 解压&#xff1a;tar -xzvf mysql-8.1.0-1.el7.x86_64.…

国家高等教育智慧教育平台

文章目录 1. 网站地址2. 网站简介3. 网站集合的资源与依托平台彩蛋环节a. 考试酷b. 公益学术平台 足不出户&#xff0c;就能免费学习2.7万门大学课程。包含国家精品课程&#xff0c;部分课程由国家级名师 / 院士 授课。 1. 网站地址 国家高等教育智慧教育平台网址&#xff1a;…

架构的本质是什么?

最近总是有小伙伴问我&#xff0c;如何成长为一名优秀的架构师&#xff0c;我也不知道该如何去回答&#xff0c;但是我想聊一下架构的本质。 架构不是互联网行业独有的 架构及对应的架构师职位并不是互联网行业独有的&#xff0c;只要存在组织的地方就存在架构。 比如一个木…

【C++】vector

文章目录 1. vector 的介绍2. vector 的使用2.1 vector 的定义2.2 vector iterator 的使用2.3 vector 的空间增长问题2.3 vector 增删查改 1. vector 的介绍 vector的文档介绍 vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用连续的存储空间来存储…

程序员必知!责任链模式的实战应用与案例分析

责任链模式让多个对象依次处理请求&#xff0c;降低发送者和接收者的耦合度&#xff0c;以在线购物为例&#xff0c;用户提交订单需经多步验证&#xff0c;通过责任链模式&#xff0c;验证器按顺序处理请求&#xff0c;先用户身份&#xff0c;再支付方式&#xff0c;最后配送地…

【LMM 011】MiniGPT-5:通过 Generative Vokens 进行交错视觉语言生成的多模态大模型

论文标题&#xff1a;MiniGPT-5: Interleaved Vision-and-Language Generation via Generative Vokens 论文作者&#xff1a;Kaizhi Zheng* , Xuehai He* , Xin Eric Wang 作者单位&#xff1a;University of California, Santa Cruz 论文原文&#xff1a;https://arxiv.org/ab…

java的面向对象编程(oop)概述及案例

前言&#xff1a; 学到了面向对象编程&#xff0c;整理下相关知识点。打牢基础&#xff0c;daydayup! 面向对象基础概述 1&#xff0c;什么是面向对象&#xff0c;面向对象的思考逻辑是什么&#xff1f; 面向对象就是把任何事物都看成一个又一个对象。思考逻辑为万物皆对象&am…

计算机毕业设计 基于javaweb的宠物认养系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

STHS34PF80人体存在传感器(1)----获取人体存在状态

STHS34PF80人体存在传感器.1--获取人体存在状态 概述视频教学样品申请完整代码下载主要特点硬件准备接口最小系统图生成STM32CUBEMX串口配置IIC配置CS设置串口重定向参考案例获取ID温度测量滤波方式智能识别算法使用块数据更新&#xff08;BDU&#xff09;功能设置ODR速率获取状…