提供三方API接口、调用第三方接口API接口、模拟API接口(二)通过token实现防止业务接口的重复调用

news2024/11/23 21:03:08

背景:紧接着上一篇,API中的签名认证,我通过signature签名机制保证了,参数不被修改,但是如果我们提供给外部的接口(此时我们作为第三方),如果被外部恶意重复调用怎么办?
此时,我们可以保证请求参数不会被修改,但这不能保证接口不被重复调用,因此还需要token、timestamp来辅助。

## 加入token防止表单重复提交

模拟重复提交表单

测试控制器(模拟高并发测试接口)

package com.atguigu.signcenter.controller;

import com.atguigu.signcenter.service.SecurityUtilTestService;
import com.atguigu.signcenter.service.serviceImpl.SecurityUtilTestServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

/**
 *    原文链接:https://blog.csdn.net/weixin_47560078/article/details/118222785
 *   @author: jd
 * @create: 2024-07-30
 */

@Slf4j
@Controller
public class TestController {


    @Autowired
    private SecurityUtilTestService securityUtilTestService;

    /**
     * 模拟高并发重复提交的现象,
     * 从而引出解决办法:加入token防止表单重复提交
     * @param data
     * @return
     */
    @PostMapping("/form/repeatSubmitTest")
    @ResponseBody
    public Map<String, Object> repeatSubmitTest(@RequestParam Map<String, String> data) {
        // 模拟提交表单信息
        Map<String, Object> result = new HashMap<>();
        result.put("code", 0);// 状态码
        result.put("msg", "success");// 信息
        log.info("提交表单[]");
        return result;
    }


}

开100个线程请求测试接口
在这里插入图片描述

测试结果:

一共100个输出,这里导致了重复调用。在实际业务中需要避免这样的问题,如果是金钱上的扣减操作,这样会导致很严重的问题

提交表单[]
2024-08-02 16:21:29.167  INFO 17420 --- [io-8025-exec-32] c.a.s.controller.TestController          : 提交表单[]
2024-08-02 16:21:29.170  INFO 17420 --- [nio-8025-exec-3] c.a.s.controller.TestController          : 提交表单[]
2024-08-02 16:21:29.170  INFO 17420 --- [io-8025-exec-40] c.a.s.controller.TestController          : 提交表单[]
2024-08-02 16:21:29.170  INFO 17420 --- [io-8025-exec-33] c.a.s.controller.TestController          : 提交表单[]
2024-08-02 16:21:29.170  INFO 17420 --- [io-8025-exec-31] c.a.s.controller.TestController          : 提交表单[]
2024-08-02 16:21:29.170  INFO 17420 --- [io-8025-exec-17] c.a.s.controller.TestController          : 提交表单[]
2024-08-02 16:21:29.171  INFO 17420 --- [io-8025-exec-38] c.a.s.controller.TestController          : 提交表单[]
2024-08-02 16:21:29.172  INFO 17420 --- [io-8025-exec-36] c.a.s.controller.TestController          : 提交表单[]
2024-08-02 16:21:29.170  INFO 17420 --- [io-8025-exec-43] c.a.s.controller.TestController          : 提交表单[]

解决表单重复提交思路
前端在提交表单之前,先调用后端接口获取临时全局唯一的token【这里可以理解成登录操作,登录获取token】,然后再postman中调用另外一个接口时,将token存入header,最后才提交表单。

后端生成token时,将token暂时缓存在redis中,设置一个有效期。当后端收到表单提交的请求时,先判断header的 token 是否在缓存中:

猜测的现象:

  • 如果业务操作的请求中未携带token,则直接返回,代表未登录。登录后重新调用业务接口。
  • 如果业务操作的请求中携带了token,则验证redis中存储的token和我携带的是否一致,如果一致,则继续处理业务逻辑,并且处理完业务后,删除缓存中的 token(这样如果重复调用的话,不再次调用getToken的话,则无法再次做业务操作,代表重复调用,这样就达到了避免重复调用的问题);如果不一致,则代表当前获取token后对业务的调用是不合法的,可能调用业务携带的token被修改过。
  • 如果redis中无token,不让调用业务接口,问题说明,情况一:登录已经失效或者根本就未登录。情况二:之前已经提交过了,redis中的被删除了,这次是重复调用业务了,这2种的需要重新获取token后携带新token进行业务调用。

加入依赖
redis依赖

    <!--        redisson依赖-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.15.5</version>
        </dependency>
        <!--redis链接客户端-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>

封装的 Redis 操作类

package com.atguigu.signcenter.util;

import com.mysql.cj.util.StringUtils;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * redis操作工具类,设置缓存,取出缓存、删除缓存操作
 *https://blog.csdn.net/weixin_47560078/article/details/118222785
 * @author: jd
 * @create: 2024-08-02
 */
@Component
public class RedisTemplateUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     *  设置缓存
     * @param key 键
     * @param data 数据
     * @param timeout 失效时间
     * @return
     */
    public Boolean setString(String key,Object data,Long timeout){
        if(data instanceof String){
            if(null!=timeout){
                stringRedisTemplate.opsForValue().set(key,(String)data,timeout, TimeUnit.SECONDS);
            }else {
                stringRedisTemplate.opsForValue().set(key, (String) data);
            }
            return true;
        }else {
            return false;
        }

    }


    /**
     * 取缓存
     * @param key 缓存键
     * @return
     */
    public String getString(String key){
        return stringRedisTemplate.opsForValue().get(key);
    }


    /**
     * 删除某个 key
     * @param key
     */
    public void delKey(String key){
        stringRedisTemplate.delete(key);
    }


}

token 工具类

package com.atguigu.signcenter.util;

import com.mysql.cj.util.StringUtils;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Objects;
import java.util.UUID;

/**
 * https://blog.csdn.net/weixin_47560078/article/details/118222785
 * token处理类
 * @author: jd
 * @create: 2024-08-02
 */

@Component
public class TokenUtil {

    @Autowired
    private RedisTemplateUtil redisTemplateUtil;

    // 时间为 秒L ,如 30分钟 应为 60*30L ,这里设置 1分钟
    private static final Long TIMEOUT = 60*2L;

    /**
     * 生成 token
     * @return
     */
    public String getToken(){
        StringBuilder token = new StringBuilder("token_");
        token.append(UUID.randomUUID().toString().replaceAll("-",""));
        redisTemplateUtil.setString(token.toString(),token.toString(),TIMEOUT);
        return token.toString();
    }


    /**
     * 判断是否有 token ,注意这个方法不验证token是否正确
     * @param tokenKey
     * @return
     */
    public String findToken(String tokenKey){
        if(Objects.nonNull(tokenKey)){
            String token = redisTemplateUtil.getString(tokenKey);
            return token;
        }
        return null;
    }


    /**
     * 删除某个 key
     * @param key
     */
    public void deleteKey(String key) {
        redisTemplateUtil.delKey(key);
    }


}

测试控制器
获取token接口、模拟业务接口:

package com.atguigu.signcenter.controller;

import com.atguigu.signcenter.util.TokenUtil;
import com.mysql.cj.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * https://blog.csdn.net/weixin_47560078/article/details/118222785
 * 模拟业务接口
 * 获取token接口、模拟业务接口:
 * @author: jd
 * @create: 2024-08-02
 */

@Slf4j
@RestController
public class tokenController {

    @Autowired
    private TokenUtil tokenUtil;

    /**
     *
     *
     * 获取 token ,这一步骤会向redis中写写入token
     * 这个可以理解成登录操作,如果登录了则会向redis中写入一个token,然后下面方法中每一次做业务,都会验证携带的token是否和redis中的一样,一样才可以操作,否则不可以做业务,需要重新登录后再做。
     * @return
     */
    @GetMapping("/getToken")
    public String getToken(){
        return tokenUtil.getToken();
    }


    /**
     * 模拟高并发重复提交,根据 token 缓存防止重复提交[最初@GetMapping("/getToken")获取token的请求,会向redis中写入一个token值]
     * 这个请求中每访问一次,redis中如果有token会做完业务后删除,如果redis中没有token,则不让做业务。
     * @param request
     * @param data
     * @return
     */
    @PostMapping("/form/repeatSubmitTest2")
    public Map<String,Object> repeatSubmitTest(HttpServletRequest request,@RequestParam Map<String, String> data){
        // 返回信息
        Map<String, Object> result = new HashMap<>();
        String paramToken = request.getHeader("token");
        //如果请求头中有token则进入到下面的业务操作中
        if(!StringUtils.isNullOrEmpty(paramToken)){
           //去找redis中是否有这个键对应的token
            String redisToken = tokenUtil.findToken(paramToken);
            //如果有redis中的token 非空,则进入
            if(!StringUtils.isNullOrEmpty(redisToken)){
                //如果参数中和redis中都有token,则验证这两个token是否一致,如果一致则可以进行业务。
                if(!paramToken.equals(redisToken)){
                    log.info("token信息错误,请重新登录后操作");
                    result.put("code", -2);// 状态码
                    result.put("msg", "登录过期,请登录后操作业务,");// 信息 这里也就是调用完getToken() 后操作业务。  这个相当于登录。
                }
                // 模拟提交表单信息
                // TODO Something
                result.put("code", 0);// 状态码
                result.put("msg", "业务操作成功");// 信息
                log.info("操作业务[]");
                // 删除缓存中的token ,正常的登录token是不用删除的,过期后自动删除,这里是为了避免重复提交,所以这里加了这个限制
                tokenUtil.deleteKey(paramToken);
            }else {
                log.info("请勿重新操作业务");
                result.put("code", -1);// 状态码
                result.put("msg", "请勿重新操作业务");// 信息
            }
        }else {
            log.info("表单无token[]");
            result.put("code", -1);// 状态码
            result.put("msg", "参数未携带token,请携带token后操作");// 信息
        }
        return result;

    }


}

测试
header无token时:业务逻辑不会执行
调用请求 : http://localhost:8025/form/repeatSubmitTest2
在这里插入图片描述
结果:
在这里插入图片描述

调用获取token接口后,再调用业务
http://localhost:8025/getToken
在这里插入图片描述

拿到token (token_5e2eeb9bc7cd468c919a57a1887502a3),放到业务请求的header中 ,发送业务请求:
在这里插入图片描述
结果:
在这里插入图片描述

2024-08-02 16:42:19.329  INFO 17420 --- [io-8025-exec-52] c.a.s.controller.tokenController         : 提交表单[]

我们将业务请求中携带的token信息修改成错误的,再次发送业务请求,
在这里插入图片描述

结果:提示重复操作,或者登录失效,请重新登录。
在这里插入图片描述
至此,通过token控制重复提交基本实现,不足的地方还请大家多多指教。
参考链接:https://blog.csdn.net/weixin_47560078/article/details/118222785

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

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

相关文章

并行编程实战——TBB中的图

一、graph 在TBB框架中&#xff0c;基础的运行框架就是图graph。简单的回顾一下什么是图&#xff1f;图是由顶点和边组成的数学结构&#xff0c;表示对象及其之间的关系。图分为有向图和无向图。在TBB中&#xff0c;其实它的图叫做流图&#xff08;Flow Graph&#xff09;&…

【leetcode详解】直角三角形:用空间换时间(O(m*n*(m+n))>O(m*n))(思路详解)

思路详解&#xff1a; 0. 遍历矩阵grid中每个点&#xff0c;若为“1”&#xff0c;则尝试将其视为直角三角形的直角顶点&#xff0c;关注该点所在横、纵轴&#xff0c;是否有其他点为“1”&#xff08;来与之构成直角边&#xff09; 1. 关于如何计算以该点为直角顶点的直角三…

【Python实战】一键生成 PDF 报告,图文并茂,代码全公开

话接上篇&#xff1a; 自动化处理 PDF 文档&#xff0c;完美实现 WPS 会员功能如何优雅地实现 PDF 去水印&#xff1f; 后台有小伙伴们问&#xff1a;能否基于已有的内容&#xff08;文本、图像等&#xff09;&#xff0c;一键生成 PDF 文档&#xff1f; 或者说&#xff0c;…

性能测试强化训练营*-可看(随意)

一.性能测试:目的/意义&#xff0c;误区 功能测试 VS 性能测试:测试一辆汽车: 功能: 轮子转不转&#xff0c;方向盘转向动不动&#xff0c;点火能不能打开发动机… --使用者&#xff0c;功能能否按照我的想法去正常使用(应用) 性能: 噪音大不大&#xff0c;百公里加速多少秒&a…

会员制重启却陷“过期门”,盒马鲜生扩张背后隐忧重重

在新零售浪潮中&#xff0c;盒马鲜生曾以“新鲜每一刻”为口号&#xff0c;迅速崛起并赢得了众多消费者的青睐。然而&#xff0c;随着其会员制的重启&#xff0c;一系列食品安全问题却如同阴霾般笼罩在这家新零售巨头的上空&#xff0c;让新老会员倍感失望与不安。 近日&#x…

跳表Java

跳表&#xff08;Skip List&#xff09;是一种用于有序数据存储的数据结构&#xff0c;它在链表的基础上增加了多级索引&#xff0c;从而提高了查找、插入和删除操作的效率。跳表的平均时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)&#xff0c;与平衡二叉搜索树&#xff08…

编程小白如何成为大神?——新生入门指南

编程小白如何成为大神&#xff1f;——新生入门指南 作为一名已经从985高校毕业的研究生&#xff0c;我深刻体会到编程已成为当代大学生的必备技能。无论是为了学术研究&#xff0c;还是未来的职业发展&#xff0c;掌握编程都能为我们提供更多的机会和竞争优势。然而&#xff…

vscode启动不了的问题解决

1、安全模式下启动vscode从中查看日志&#xff1a; code --verbose at Ce.d (C:\Users\yonghu\AppData\Local\Programs\Microsoft VS Code\resources\app\out\vs\code\electron-main\main.js:116:3783)at Ce.a (C:\Users\yonghu\AppData\Local\Programs\Microsoft VS Code\res…

ts保姆级学习指南

什么是 TypeScript&#xff1f; TypeScript&#xff0c;简称 ts&#xff0c;是 JavaScript 的超集&#xff0c;而且它最大的特点之一就是引入了静态类型支持。这意味着开发者可以在 TypeScript 中定义变量、函数参数等的类型&#xff0c;编译器会在编译时进行类型检查&#xf…

Ubuntu配置Ngbatis学习环境

引言 经过考虑&#xff0c;我感觉与NebulaGraph交互的ORM框架还是Ngbatis好。因为现在这个框架开发的比较完善&#xff0c;而且还在不断更新&#xff0c;社区活跃的用户多。从今日开始学习&#xff0c;首先要配置一下环境。 1.安装maven和jdk 选择的版本是maven3.8和jdk17.以…

iPhone可运行的谷歌Gemma 2 2B模型,性能超GPT-3.5

在数字化浪潮的推动下&#xff0c;人工智能&#xff08;AI&#xff09;正成为塑造未来的关键力量。硅纪元视角栏目紧跟AI科技的最新发展&#xff0c;捕捉行业动态&#xff1b;提供深入的新闻解读&#xff0c;助您洞悉技术背后的逻辑&#xff1b;汇聚行业专家的见解&#xff0c;…

关于inet_addr()中的参数不能是 sring类型的 只能是 string类型变量.c_str()

源码展示&#xff1a; extern in_addr_t inet_addr (const char *__cp) __THROW inet_addr中的参数是const char *类型的 定义一个string 类型的ip 使用这个inet_addr()接口 local.sin_addr.s_addr inet_addr(ip_.c_str()); local.sin_addr.s_addr inet_addr(&ip_);…

ELK对业务日志进行收集

ELK对业务日志进行收集 下载httpd 进到文件设置收集httpd的文件进行 设置 编辑内容 用于收集日志的内容 将日志的内容发送到实例当中 input {file{path > /etc/httpd/logs/access_logtype > "access"start_position > "beginning"}file{path &g…

基于SpringBoot+Vue的健身俱乐部网站(带1w+文档)

基于SpringBootVue的健身俱乐部网站(带1w文档) 基于SpringBootVue的健身俱乐部网站(带1w文档) 该系统采用java技术&#xff0c;结合ssm框架使页面更加完善&#xff0c;后台使用MySQL数据库进行数据存储。系统主要分为三大模块&#xff1a;即管理员模块和用户模块、教练模块。本…

openstack之nova-conductor工作原理及常见问题处理

openstack之nova-conductor工作原理及常见问题处理 这里写目录标题 openstack之nova-conductor工作原理及常见问题处理一、简介1. 概念2. 作用3. 体系结构 二、组件1. nova-api2. nova-scheduler3. nova-compute4. nova-conductor5. nova-api-metadata6. nova-placement-api7. …

Java AI伪原创视频创作视频提取文案改写去水印系统小程序源码

&#x1f525;AI赋能创作新纪元&#xff01;伪原创视频文案提取改写去水印全能系统大揭秘 &#x1f680; 开篇&#xff1a;创意无界&#xff0c;AI来助力 在这个视觉盛行的时代&#xff0c;视频创作成为了表达自我、传递信息的重要方式。但你是否曾为寻找灵感、撰写文案、处理…

SD-WAN的两种方案及其价值

SD-WAN&#xff08;软件定义广域网&#xff09;作为一种新兴的网络架构解决方案&#xff0c;给企业网络带来了极大的灵活性和可扩展性。它允许企业以更低的成本将广泛分布的分支机构连接到数据中心或其他分支机构&#xff0c;同时还能优化网络性能和用户体验。 SD-WAN的工作原理…

C语言之“文件操作”

文章目录 1. 什么是文件&#xff1f;&#xff08;1. 为什么使用文件&#xff1f;&#xff08;2.什么是文件2.1 程序文件2.2 数据文件2.3 文件名 2. 二进制文件和文本文件&#xff1f;3. 文件的打开和关闭3.1 流和标准流3.2 文件指针(用来管理流的)3.3 文件的打开与关闭 4. 文件…

vitis (eclipse) 的Indexer不能搜索、不能跳转到函数和变量定义和声明不能打开调用层次的解决方法

在使用vitis(2021.1) 过程中&#xff0c;有一个非常方便实用的功能&#xff0c;就是在函数或变量等源代码上通过右键菜单或快捷键F3、F4、CtrlAltH&#xff0c;也可以按住Ctrl键然后鼠标停留在函数名或变量名上&#xff0c;点击出现的链接&#xff0c;可以跳转到函数或变量的定…

linux磁盘可视化分析工具

在 Linux 系统中&#xff0c;了解磁盘使用情况对于系统维护和优化至关重要。文件和目录随着时间的推移会占据大量磁盘空间&#xff0c;了解哪些部分占用的空间最多可以帮助我们更好地管理和清理磁盘。Baobab&#xff0c;也称为 GNOME Disk Usage Analyzer&#xff0c;是一款非常…