SpringBoot中,接口签名,通用方案,以确保接口的安全性

news2024/11/28 13:36:01

1. 为什么需要接口签名?

  • 接口签名目的:防止第三方伪造请求。
  • 请求伪造:未经授权的第三方构造合法用户的请求来执行不希望的操作。
  • 转账接口示例:展示了如果接口没有安全措施,第三方可以轻易伪造请求,例如将资金从一个账户转移到另一个账户。

2. 如何实现接口签名?

  • 引入密钥:接口调用方和服务提供方之间共享一个密钥(secretKey),此密钥必须保密。
  • 签名算法:使用密钥和请求体的内容通过MD5算法生成签名。
  • 携带签名:客户端在请求头中附加生成的签名。
  • 服务端校验:服务器接收到请求后,使用同样的算法和密钥重新计算签名并与请求中提供的签名比较,如果不一致,则拒绝请求。

3. 防止请求伪造

  • 请求伪造解决办法:通过接口签名机制,第三方不知道密钥,因此无法正确生成匹配的签名,请求会被服务器拒绝。

4. 防止请求重放

  • 请求重放定义:攻击者截获合法请求后重新发送以达到重复执行的效果。
  • 解决请求重放的办法:引入随机字符串(nonce)和时间戳(timestamp)。nonce用来确保每个请求只能被使用一次,存储在Redis中并在一段时间后过期;时间戳用来限制请求的有效时间范围。





具体实现

1 整合springboot+redis环境

2 pom.xml

<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.2</version>
</dependency>

3 yml配置

//redis 相关的配置省略了

//秘钥,要保密
secret-key: b0e8668b-bcf2-4d73-abd4-893bbc1c6079

4 类ReusableBodyRequestWrapper,该类用于包装HttpServletRequest,以便在读取请求体后仍可重复读取

import org.apache.tomcat.util.http.fileupload.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

/**
 * 该类用于包装HttpServletRequest,以便在读取请求体后仍可重复读取
 */
public class ReusableBodyRequestWrapper extends HttpServletRequestWrapper {

    //参数字节数组,用于存储请求体的字节数据
    private byte[] requestBody;

    //Http请求对象
    private HttpServletRequest request;
    
    /**
     * 构造函数,初始化包装类
     * @param request 原始HttpServletRequest对象
     * @throws IOException 如果读取请求体时发生IO错误
     */
    public ReusableBodyRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.request = request;
    }
    
    /**
     * 重写getInputStream方法,实现请求体的重复读取
     * @return 包含请求体数据的ServletInputStream对象
     * @throws IOException 如果读取请求体时发生IO错误
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        /**
         * 每次调用此方法时将数据流中的数据读取出来,然后再回填到InputStream之中
         * 解决通过@RequestBody和@RequestParam(POST方式)读取一次后控制器拿不到参数问题
         */
        //仅当requestBody未初始化时,从请求中读取并存储到requestBody
        if (null == this.requestBody) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            IOUtils.copy(request.getInputStream(), baos);
            this.requestBody = baos.toByteArray();
        }
        //创建一个 ByteArrayInputStream 对象,用于重复读取requestBody
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
        return new ServletInputStream() {

            @Override
            public boolean isFinished() {
                //始终返回false,表示数据流未完成
                return false;
            }

            @Override
            public boolean isReady() {
                //始终返回false,表示数据流未准备好
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {
                //不执行任何操作,因为该数据流不支持异步操作
            }

            @Override
            public int read() {
                //从ByteArrayInputStream中读取数据
                return bais.read();
            }
        };
    }
    
    /**
     * 获取请求体的字节数组
     * @return 请求体的字节数组
     */
    public byte[] getRequestBody() {
        return requestBody;
    }
    
    /**
     * 重写getReader方法,返回一个基于getInputStream的BufferedReader
     * @return 包含请求体数据的BufferedReader对象
     * @throws IOException 如果读取请求体时发生IO错误
     */
    @Override
    public BufferedReader getReader() throws IOException {
        //基于getInputStream创建BufferedReader
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

5 SignatureVerificationFilter类,签名验证过滤器,用于校验请求的合法性

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * 签名验证过滤器,用于校验请求的合法性
 */
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter(urlPatterns = "/**", filterName = "SignatureVerificationFilter")
@Component
public class SignatureVerificationFilter extends OncePerRequestFilter {
    public static Logger logger = LoggerFactory.getLogger(SignatureVerificationFilter.class);
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 对request进行包装,支持重复读取body
        ReusableBodyRequestWrapper requestWrapper = new ReusableBodyRequestWrapper(request);
        // 校验签名
        if (this.verifySignature(requestWrapper, response)) {
            filterChain.doFilter(requestWrapper, response);
        }
    }
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 签名秘钥
    @Value("${secret-key}")
    private String secretKey;
    
    /**
     * 校验签名
     *
     * @param request  HTTP请求
     * @param response HTTP响应
     * @return 签名验证结果
     * @throws IOException 如果读取请求体失败
     */
    public boolean verifySignature(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 签名
        String sign = request.getHeader("X-Sign");
        // 随机数
        String nonce = request.getHeader("X-Nonce");
        // 时间戳
        String timestampStr = request.getHeader("X-Timestamp");
        if (!StringUtils.hasText(sign) || !StringUtils.hasText(nonce) || !StringUtils.hasText(timestampStr)) {
            this.write(response, "参数错误");
            return false;
        }
        
        // timestamp 10分钟内有效
        long timestamp = Long.parseLong(timestampStr);
        long currentTimestamp = System.currentTimeMillis() / 1000;
        if (Math.abs(currentTimestamp - timestamp) > 600) {
            this.write(response, "请求已过期");
            return false;
        }
        
        // 防止请求重放,nonce只能用一次,放在redis中,有效期 20分钟
        String nonceKey = "SignatureVerificationFilter:nonce:" + nonce;
        if (!this.redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", 20, TimeUnit.MINUTES)) {
            this.write(response, "nonce无效");
            return false;
        }
        
        // 请求体
        String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
        // 需要签名的数据:secretKey+noce+timestampStr+body
        // 校验签名
        String data = String.format("%s%s%s%s", this.secretKey, nonce, timestampStr, body);
        if (!DigestUtil.md5Hex(data).equals(sign)) {
            write(response, "签名有误");
            return false;
        }
        return true;
    }
    
    /**
     * 向客户端写入响应信息
     *
     * @param response HTTP响应
     * @param msg      响应信息
     * @throws IOException 如果写入失败
     */
    private void write(HttpServletResponse response, String msg) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().write(JSONUtil.toJsonStr(msg));
    }
}

6 自己写的一个生成签名的工具类,可选项,因为在实现中,应该是前台传参或代码里写的,这是只是方便测试调度调试

import cn.hutool.crypto.digest.DigestUtil;
import org.springframework.util.StringUtils;
import java.util.UUID;

public class SignatureUtil {
    
    /**
     * 生成签名
     *
     * @param body      请求体
     * @param secretKey 密钥
     * @param nonce     随机数
     * @param timestamp 时间戳
     * @return 签名
     */
    public static String generateSignature(String body, String secretKey, String nonce, String timestamp) {
        if (!StringUtils.hasText(body) || !StringUtils.hasText(secretKey) || !StringUtils.hasText(nonce) || !StringUtils.hasText(timestamp)) {
            throw new IllegalArgumentException("参数不能为空");
        }
        
        // 按照 secretKey + nonce + timestamp + body 的顺序拼接字符串
        String data = String.format("%s%s%s%s", secretKey, nonce, timestamp, body);
        System.out.println("data = " + data);
        
        // 使用MD5算法计算签名
        String sign = DigestUtil.md5Hex(data);
        
        return sign;
    }
    
    public static void main(String[] args) {
        // 示例参数
        String body = "{\n" +
                "  \"fromAccountId\": \"张三\",\n" +
                "  \"toAccountId\": \"李四\",\n" +
                "  \"transferPrice\": 100\n" +
                "}";
        
        //秘钥
        String secretKey = "b0e8668b-bcf2-4d73-abd4-893bbc1c6079";
        // 随机数
        String nonce = UUID.randomUUID().toString().replace("-", "");
        // 时间戳
        long timestamp = System.currentTimeMillis() / 1000;
        
        // 生成签名
        String sign = generateSignature(body, secretKey, nonce, String.valueOf(timestamp));
        
        // 输出生成的签名
        System.out.println("X-Sign: " + sign);
        System.out.println("X-Nonce: " + nonce);
        System.out.println("X-Timestamp: " + timestamp);
    }
}

7 写一个接口,用于调用

import lombok.*;
import java.math.BigDecimal;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TransferRequest {
    //付款人账户id
    private String fromAccountId;
    //收款人账号id
    private String toAccountId;
    //转账金额
    private BigDecimal transferPrice;
}

///

import cn.hutool.json.JSONUtil;
import com.example.demo_26.dto.TransferRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class AccountController {
    @RequestMapping("/account/transfer")
    public Object transfer(@RequestBody TransferRequest request) {
        log.info("转账成功:{}", JSONUtil.toJsonStr(request));
        return "转账成功";
    }
}

8 测试
最后的效果,只能发一次请求,重复发送请求,就会失败,需要用新的随机数,时间戳,生成新的签名才可以

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

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

相关文章

用户在网页上输入一个网址,它整个页面响应的流程是什么?

目录 一、流程的大致过程 二、流程的详细分析 1. 浏览器先分析超链接中的URL 2. DNS解析 3. 建立TCP连接 建立连接&#xff08;三次握手&#xff09; HTTP中的请求报文 4. 浏览器发送HTTP请求 5. 服务器处理请求并发送响应 HTTP的响应报文 6. 浏览器接收响应 7. 渲…

After-kaoyan

知乎 - 安全中心 有态度&#xff0c;有回应&#xff0c;有温度&#xff0c;是跟双鱼相处的基础 我今天跟大家泄漏一个秘密&#xff0c;这个秘密也很简单&#xff0c;就是我每次遇到困难险阻时候我从不退缩&#xff0c;我也不会想着&#xff1a;“算了吧&#xff0c;我做不到&a…

基于Springboot+Vue的零食批发商仓库管理系统(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 在这个…

Python调试技巧:高效定位与修复问题

Python调试技巧&#xff1a;高效定位与修复问题 在Python编程过程中&#xff0c;调试是不可避免的重要环节。无论是刚接触编程的初学者还是经验丰富的开发者&#xff0c;都可能会遇到代码运行不符合预期的情况。高效的调试技巧不仅能帮助我们快速找到问题&#xff0c;还能减少…

Graphiti:如何让构建知识图谱变得更快、更具动态性?

扩展大语言模型数据提取&#xff1a;挑战、设计决策与解决方案 Graphiti 是一个用于构建和查询动态、时间感知的知识图谱的 Python 库。它可以用于建模复杂、不断演变的数据集&#xff0c;并确保 AI 智能体能够访问它们完成非平凡任务所需的数据。它是一个强大的工具&#xff…

9个微服务最佳实践

1⃣分离数据存储&#xff1a;独立数据库&#xff0c;提升灵活性。 2⃣代码成熟度一致&#xff1a;质量稳定&#xff0c;避免技术债务 3⃣独立构建流程&#xff1a;独自构建&#xff0c;快速部署。 4⃣单一职责原则&#xff1a;业务功能单一&#xff0c;简化维护。 5⃣容器化部署…

Android车载——VehicleHal初始化(Android 11)

1 概述 VehicleHal是AOSP中车辆服务相关的hal层服务。它主要定义了与汽车硬件交互的标准化接口和属性管理&#xff0c;是一个独立的进程。 2 进程启动 VehicleHal相关代码在源码树中的hardware/interfaces/automotive目录下 首先看下Android.bp文件&#xff1a; cc_binary …

大模型公司对标:360

公司档案 360成立于2005年&#xff0c;初期以提供免费的杀毒软件“360安全卫士”而迅速获得市场认可&#xff0c;并逐渐发展成为一家提供全面互联网安全解决方案的企业。2015年成立人工智能研究院&#xff0c;开展人工智能技术探索&#xff0c;成为国内布局研究开发人工智能较…

Oracle 表空间异构传输

已经有了表空间的数据文件&#xff0c;和元数据dump文件&#xff0c;如何把这个表空间传输到异构表空间中&#xff1f; 查询异构传输平台信息&#xff1a; COLUMN PLATFORM_NAME FORMAT A40 SELECT PLATFORM_ID, PLATFORM_NAME, ENDIAN_FORMAT FROM V$TRANSPORTABLE_PLATFORM O…

教育技术革新:SpringBoot在线教育系统开发指南

6系统测试 6.1概念和意义 测试的定义&#xff1a;程序测试是为了发现错误而执行程序的过程。测试(Testing)的任务与目的可以描述为&#xff1a; 目的&#xff1a;发现程序的错误&#xff1b; 任务&#xff1a;通过在计算机上执行程序&#xff0c;暴露程序中潜在的错误。 另一个…

计算机找不到vcomp140.dll,无法继续执行代码如何解决,有什么好的修复方法

1. vcomp140.dll 简介 1.1 定义 vcomp140.dll 是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;它属于 Microsoft Visual C 2015 Redistributable Package 的一部分。该文件为应用程序提供了 OpenMP 并行框架所需的运行时支持&#xff0c;允许开发者编写并发和多…

【Verilog学习日常】—牛客网刷题—Verilog进阶挑战—VL25

输入序列连续的序列检测 描述 请编写一个序列检测模块&#xff0c;检测输入信号a是否满足01110001序列&#xff0c;当信号满足该序列&#xff0c;给出指示信号match。 模块的接口信号图如下&#xff1a; 模块的时序图如下&#xff1a; 请使用Verilog HDL实现以上功能&#x…

论文笔记:微表情欺骗检测

整理了AAAI2018 Deception Detection in Videos 论文的阅读笔记 背景模型实验可视化 背景 欺骗在我们的日常生活中很常见。一些谎言是无害的&#xff0c;而另一些谎言可能会产生严重的后果。例如&#xff0c;在法庭上撒谎可能会影响司法公正&#xff0c;让有罪的被告逍遥法外。…

电脑获得高级管理员权限(Windows10 专业版)

电脑获得高级管理员权限(Windows10 专业版) 请谨慎操作 通常我们在删除一些文件时&#xff0c;会提示权限不足&#xff0c;删除不了文件 我们可以打开组策略编辑器将当前用户修改为高级管理员权限 Windows10获取高级管理员权限 首先打开本地组策略编辑器(cmd输入gpedit.msc)其…

20分钟写一个链表

目录 前言1.带头结点的循环双链表1.1 链表的分类、线性表的对比1.2 双链表基本操作代码实现1.2.1 初始化1.2.2 销毁、打印链表 总结 前言 有一个学长在面试的时候被问到这样一个问题&#xff0c;“你可以用20分钟写一个链表吗&#xff1f;”学长第一反应是&#xff0c;至少要一…

传统图像处理Opencv分割不同颜色的夹子

任务要求&#x1f349; 1. 计算图像中夹子的总数。 2. 分别计算不同颜色夹子的个数。 3. 使用以下方法适应三张图片&#xff0c;并在每张图像上显示结果&#xff1a; - 阈值方法 - HSV颜色空间 - 连通域分析 - 形态学图像处理 - Canny边缘检测 4. 在结果中显示计…

北交大研究突破:塑料光纤赋能低成本无摄像头AR/VR眼动追踪技术

北交大研究&#xff1a;探索无摄像头低成本AR/VR眼动追踪新路径 在AR/VR技术领域&#xff0c;眼动追踪作为一项关键技术&#xff0c;对于提升用户体验、优化渲染效率具有重要意义。然而&#xff0c;传统的眼动追踪方案多依赖于高成本的摄像头&#xff0c;这不仅增加了设备的制造…

学习资料库系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;观看记录管理&#xff0c;基础数据管理&#xff0c;论坛信息管理&#xff0c;公告信息管理&#xff0c;轮播图信息 微信端账号功能包括&#xff1a;系统首页&#xff0c;阅读资…

性能学习5:性能测试的流程

一.需求分析 二.性能测试计划 1&#xff09;测什么&#xff1f; - 项目背景 - 测试目的 - 测试范围 - ... 2&#xff09;谁来测试 - 时间进度与分工 - 交付清单 - ... 3&#xff09;怎么测 - 测试策略 - ... 三.性能测试用例 四.性能测试执行 五.性能分析和调优 六…

【cpp/c++ summary 工具】 conan包管理器安装与cmake项目配置

发现有些包cvpkg中没有&#xff08;比如字典树&#xff09;&#xff0c;conan里有。 安装conan https://conan.io/downloads 配置conan PS C:\Users\multisim> conan config home C:\Users\multisim\.conan2 # Conan 主文件夹的路径,https://docs.conan.io/2/referen…