五.Springboot通过AOP实现API接口的签名验证

文章目录

  • 前言
  • 一、实现原理
  • 二、签名规则
  • 三、服务端实现
    • 4.1、创建自定义注解
    • 4.2、创建切面处理类
    • 4.3、对应工具类`RequestUtil`
  • 四、测试
    • 4.1 签名失败测试:
    • 4.2 签名成功测试:
  • 四、总结


前言

对外开放的接口,需要验证请求方发送过来的数据确实是由发送方发起的,并且中途不能被篡改和伪造,所以希望对接口的访问进行签名验证,以验证请求的合法性和完整性,防止数据篡改、伪造或重放攻击,保障接口调用的安全性


一、实现原理

客户端对请求参数进行整理,并且通过MD5的方式(必须是不可逆的签名算法)生成签名标识,后端拿到请求的信息内容,经过同样的算法规则得到签名标识,比对和接收到的签名一致,则验证通过

二、签名规则

该签名规则只是作为参考,实际项目根据需要进行变更,因为规则的复杂程度决定了签名是否可以被伪造的难易程度

参数一般分为3种,分别为:
PathVariable参数
QueryParams参数
Body参数

  • 步骤1:参数排序
    将①和②类型的参数按参数名ASCII码从小到大排序(字典序,从大到小也可以,根据需求约定好即可),且参数名和参数值以键值对互相连接组成一个请求参数串paramStr,每个参数以#结尾,例如:

PathVariable有2个(subId,typeQueryParams参数有2个(status,date
1.先将参数名按照ASCII码从小到大排序,排序后为date, status, subId, type
2.再将参数名和参数值以键值对互相连接,即date=2023-01-01#status=1#subId=1001#type=2#

  • 步骤2:拼接参数
    将③body参数json字符串、当前时间戳timestamp、随机串nonce、直接拼接到paramStr字符串前面,顺序为:nonce,timestamp,body;例如:

这里可以根据项目需求,可以再添加一个密钥key加入到签名中,前后端保持一致,增加安全性

body参数为:{"name":"zhangsan","age":1},最后拼接的字符串为:nonce=KnjIHO9F4w#timestamp=1672554225456#{"name":"zhangsan","age":1}#date=2023-01-01#status=1#subId=1001#type=2#

注意:所有键值对都需要以#结尾

  • 步骤3:计算签名
    步骤2最后得到的字符串进行MD5计算,得到sign签名如下:0e72ded211cc19f3c00aeca54378d06b

  • 步骤4:携带Headers
    步骤3获取到的签名sign以及进行签名的时间戳timestamp放到Headers一起请求接口地址

三、服务端实现

服务端依然是采用AOP思想来实现对参数进行统一解析并签名对比的思路实现,其实现思路为:通过自定义注解,对需要进行验证签名的接口进行拦截,再按照签名规则进行签名验证,如果验证失败直接进行返回,验证成功则放行。话不多说,直接开干

4.1、创建自定义注解

sign包中创建自定义注解CheckSign

package com.light.common.sign;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义验证签名注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME )
public @interface CheckSign {

}

4.2、创建切面处理类

package com.light.common.sign;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.light.common.exception.ServiceException;
import com.light.common.utils.RequestUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Map;
import java.util.TreeMap;

/**
 * 签名验证的切面处理类
 */
@Aspect
@Component
@Order(3)
public class SignAspect {
    private static final Logger log = LoggerFactory.getLogger(SignAspect.class);
    //接口签名验证超时时间,默认3分钟,此处
    private final int EXPIRE_TIME = 180;


    /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(checkSign)")
    public void boBefore(JoinPoint joinPoint, CheckSign checkSign) throws ServiceException {
        log.info("开始验证签名");
        signHandle(joinPoint);
    }

    /**
     * 切面逻辑实现
     *
     * @param joinPoint 连接点
     */
    public void signHandle(JoinPoint joinPoint) throws ServiceException {
        // 获取HttpServletRequest对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 从请求头中获取timestamp和sign参数
        String timestamp = request.getHeader("timestamp");
        String sign = request.getHeader("sign");
        if (ObjectUtil.isEmpty(timestamp) || ObjectUtil.isEmpty(sign)) {
            throw new ServiceException("未检测到签名参数");
        }

        long requestTime = Long.parseLong(timestamp) / 1000; // 请求时间(秒)
        long now = System.currentTimeMillis() / 1000; // 当前时间(秒)
        // 如果当前时间小于请求时间,说明请求时间无效
        if (now < requestTime) {
            log.warn("请求时间无效,当前时间: {}, 请求时间: {}", now, requestTime);
            throw new ServiceException("请求时间无效");
        }
        // 判断请求时间与当前时间的差值是否超过过期时间
        if ((now - requestTime) > EXPIRE_TIME) {
            // 请求过期,处理过期逻辑
            log.warn("请求已过期,当前时间: {}, 请求时间: {}", now, requestTime);
            throw new ServiceException("请求已过期");
        }

        // 1. 获取并排序参数
        String paramStr = RequestUtil.getSortedParamString(joinPoint);

        // 2. 拼接Body参数和时间戳
        String bodyParam = RequestUtil.getBodyParameter(joinPoint);
        String finalStr = String.join("#", timestamp, bodyParam, paramStr);

        // 3. 计算签名
        String generatedSign = SecureUtil.md5(finalStr);
        log.warn("参数签名: {}, 计算签名: {}", sign, generatedSign);
        if (!generatedSign.equals(sign)) {
            throw new ServiceException("签名错误");
        }
    }

}

4.3、对应工具类RequestUtil

package com.light.common.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class RequestUtil {

    /**
     * 获取PathVariable和QueryParams参数,并按字典序排序拼接成字符串
     * @param joinPoint 切点
     * @return 参数拼装后的字符串
     */
    public static String getSortedParamString(JoinPoint joinPoint) {
        // 使用TreeMap自动排序
        Map<String, String> params = new TreeMap<>();

        // 获取QueryParams和PathVariable参数
        JSONObject requestParamJson = RequestUtil.getRequestParamParameterJson(joinPoint);
        if (requestParamJson != null) {
            // 如果参数不为空,将其加入到params中
            requestParamJson.forEach((key, value) -> params.put(key, value.toString()));
        }

        JSONObject pathVariableJson = RequestUtil.getPathVariableParameterJson(joinPoint);
        if (pathVariableJson != null) {
            // 如果参数不为空,将其加入到params中
            pathVariableJson.forEach((key, value) -> params.put(key, value.toString()));
        }

        // 拼接成字符串,如:date=2023-01-01#status=1#subId=1001#type=2#
        StringBuilder paramStr = new StringBuilder();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            paramStr.append(entry.getKey()).append("=").append(entry.getValue()).append("#");
        }
        return paramStr.toString();
    }

    /**
     * 获取Body参数并将其转为JSON字符串
     * @param joinPoint 切点
     * @return body参数json字符串
     */
    public static String getBodyParameter(JoinPoint joinPoint) {
        // 获取Body参数
        JSONObject bodyParamJson = RequestUtil.getRequestBodyParameterJson(joinPoint);
        return bodyParamJson != null ? JSONUtil.toJsonStr(bodyParamJson) : "";
    }

    /**
     * 获取@RequestBody参数的JSON格式对象
     *
     * @param joinPoint 连接点,包含了当前方法执行的信息,主要用于获取方法参数
     * @return 返回RequestBody参数的JSON格式对象,如果没有找到@RequestBody参数,返回null
     */
    public static JSONObject getRequestBodyParameterJson(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            Parameter parameter = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameters()[i];
            if (parameter.isAnnotationPresent(RequestBody.class)) {
                // 将RequestBody参数转换为JSON对象
                return JSONUtil.parseObj(args[i]);
            }
        }
        return null;
    }

    /**
     * 获取@RequestParam参数的JSON格式对象
     *
     * @param joinPoint 连接点,包含了当前方法执行的信息,主要用于获取方法参数
     * @return 返回RequestParam参数的JSON格式对象,如果没有找到@RequestParam参数,返回null
     */
    public static JSONObject getRequestParamParameterJson(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Parameter[] parameters = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameters();
        for (int i = 0; i < parameters.length; i++) {
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                Map<String, Object> map = new HashMap<>();
                String key = parameters[i].getName();
                if (!StrUtil.isEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                if (args[i] != null) {
                    map.put(key, args[i]);
                }
                // 将@RequestParam参数转换为JSON对象
                return JSONUtil.parseObj(map);
            }
        }
        return null;
    }

    /**
     * 获取@PathVariable参数的JSON格式对象
     *
     * @param joinPoint 连接点,包含了当前方法执行的信息,主要用于获取方法参数
     * @return 返回PathVariable参数的JSON格式对象,如果没有找到@PathVariable参数,返回null
     */
    public static JSONObject getPathVariableParameterJson(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Parameter[] parameters = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameters();
        for (int i = 0; i < parameters.length; i++) {
            PathVariable pathVariable = parameters[i].getAnnotation(PathVariable.class);
            if (pathVariable != null) {
                Map<String, Object> map = new HashMap<>();
                String key = parameters[i].getName();
                if (!StrUtil.isEmpty(pathVariable.value())) {
                    key = pathVariable.value();
                }
                if (args[i] != null) {
                    map.put(key, args[i]);
                }
                // 将@PathVariable参数转换为JSON对象
                return JSONUtil.parseObj(map);
            }
        }
        return null;
    }

    /**
     * 获取所有参数的JSON格式字符串,包含@RequestBody、@RequestParam和@PathVariable注解参数
     *
     * @param joinPoint 连接点,包含了当前方法执行的信息,主要用于获取方法参数
     * @return 返回包含@RequestBody、@RequestParam和@PathVariable参数的JSON格式对象
     */
    public static JSONObject getAllParameter(JoinPoint joinPoint) {
        // 创建一个Map来保存所有参数的JSON对象
        Map<String, Object> parametersJson = new HashMap<>();

        // 获取RequestBody参数的JSON
        JSONObject requestBodyJson = getRequestBodyParameterJson(joinPoint);
        if (requestBodyJson != null) {
            parametersJson.put("bodyParam", requestBodyJson);
        } else {
            parametersJson.put("bodyParam", null);
        }

        // 获取RequestParam参数的JSON
        JSONObject requestParamJson = getRequestParamParameterJson(joinPoint);
        if (requestParamJson != null) {
            parametersJson.put("requestParam", requestParamJson);
        } else {
            parametersJson.put("requestParam", null);
        }

        // 获取PathVariable参数的JSON
        JSONObject pathVariableJson = getPathVariableParameterJson(joinPoint);
        if (pathVariableJson != null) {
            parametersJson.put("pathParam", pathVariableJson);
        } else {
            parametersJson.put("pathParam", null);
        }

        // 将所有参数的JSON格式对象拼装成一个JSONObject并返回
        return new JSONObject(parametersJson);
    }
}

四、测试

我们只需要需要进行验证签名的控制器方法中加上@CheckSign即可验证自动验证签名

    @CheckSign
    @GetMapping("/getMsg/{subId}")
    public BaseResult<?> getTest(@PathVariable String subId, @RequestParam int name,@RequestBody JSONObject postData) {
        JSONObject data = new JSONObject();
        data.put("subId", subId);
        data.put("name", name);
        data.put("postData", postData);
        return BaseResult.success(data);
    }

4.1 签名失败测试:

在这里插入图片描述

  • 控制台输出
    在这里插入图片描述

4.2 签名成功测试:

在这里插入图片描述

四、总结

对请求参数进行签名验证的方式还有多种,比如WebFilter,拦截器 InterceptorAOP,RequestBodyAdviceHandlerMethodArgumentResolver等,我们这里采用的AOP主要是希望可以灵活控制哪些接口是否需要验签,且灵活度较高,实际项目可根据需求自行选择

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

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

相关文章

记录jvm进程号

日常开发中&#xff0c;相信大家会经常用到这么一行命令&#xff1a; ps -ef | grep xxx.jar | grep -v grep | awk {print $2} | xargs -r kill -9 就是杀掉xxx相关的进程&#xff0c;然后启动&#xff0c;当然也还有其他的方式可以实现类似的功能&#xff0c;我就不列举了&…

STM32完全学习——使用定时器1精确延时

一、定时器的相关配置 首先一定要是递减定时器&#xff0c;递增的不太行&#xff0c;控制的不够准确&#xff0c;其次在大于10微秒的延时是非常准确的&#xff0c;小于的话&#xff0c;就没有那没准&#xff0c;但是凑合能用。误差都在一个微秒以内。使用高级定时器也就是时钟…

简述css中z-index的作用?如何用定位使用?

z-index是一个css属性&#xff0c;用于控制元素的堆叠顺序&#xff0c; 如何使用定位用index 1、position&#xff1a;relative&#xff1b; z-index&#xff1b; 相对于自己来定位的&#xff0c;可以根据top&#xff0c;bottom&#xff0c;right&#xff0c;left&#xff…

被裁20240927 --- 嵌入式硬件开发 STM32篇

人很容易原谅别人的错误但很难原谅别人的正确 1. 文档、手册、指南、资源2. MCU 结构3. MCU 和 MPU 的区别4. 一些概念什么是看门狗 &#xff1f;什么是 DMA &#xff1f;什么是晶振 &#xff1f;什么是片内外设&#xff1f;软件协议、硬件协议、数据协议、通讯协议、通信协议u…

被邀请出版Cursor教程书籍是什么体验?

本文目的 本次文章和大家分享一下被邀请出版书籍的经历、准备工作&#xff1b;准备工作大部分文字内容由Cursor完成。同时如果有后续、未来书籍真的出版&#xff0c;我会持续进行分享&#xff0c;欢迎关注我~ 事情来由 前几天我被出版社编辑联系&#xff0c;询问我是否可以合…

全新免押租赁系统助力商品流通高效安全

内容概要 全新免押租赁系统的推出&#xff0c;可以说是一场商品流通领域的小革命。想象一下&#xff0c;不再为押金烦恼&#xff0c;用户只需通过一个简单的信用评估&#xff0c;就能快速租到所需商品&#xff0c;这种体验简直令人惊喜&#xff01;这个系统利用代扣支付技术&a…

upload-labs关卡记录9

还是上传一个一句话木马&#xff0c;提示文件类型不允许上传&#xff0c;随变改一改后缀名&#xff0c;发现就可以上传了&#xff0c;黑名单&#xff0c;这里经过尝试&#xff0c;常规的双写后缀&#xff0c;大小写&#xff0c;.htaccess&#xff0c;然后抓包&#xff0c;试试点…

高质量 Next.js 后台管理模板源码分享,开发者必备

高质量 Next.js后台管理模板源码分享&#xff0c;开发者必备 Taplox 是一个基于 Bootstrap 5 和 Next.js 构建的现代化后台管理模板和 UI 组件库。它不仅设计精美&#xff0c;还提供了一整套易用的工具&#xff0c;适合各种 Web 应用、管理系统和仪表盘项目。无论你是初学者还是…

路由器刷机TP-Link tp-link-WDR5660 路由器升级宽带速度

何在路由器上设置代理服务器&#xff1f; 如何在路由器上设置代理服务器&#xff1f; 让所有连接到该路由器的设备都能够享受代理服务器的好处是一个不错的选择&#xff0c;特别是当需要访问特定的网站或加速网络连接的时候。下面是一些您可以跟随的步骤&#xff0c;使用路由器…

【Linux/踩坑】Linux中启动eclipse或HDFS因JAVA_HOME设置报错

Linux中启动eclipse或hadoop因JAVA_HOME设置报错 eclipseHadoop eclipse 错误提示&#xff1a; A Java Runtime Environment (JRE) or Java Development Kit (JDK) must be available in order to run Eclipse. No Java virtual machine was found after searching the follo…

可信数据空间建设及应用参考指南(V1.0)

为贯彻国家数据局《可信数据空间发展行动计划&#xff08;2024-2028 年&#xff09;》&#xff0c;促进可信数据空间持续、快速和健康发展&#xff0c;相关行业专家组织编写《可信数据空间建设及应用参考指南&#xff08;V1.0&#xff09;》&#xff08;以下简称《参考指南》&a…

Hive刷分区MSCK

一、MSCK刷分区 我们平时通常是通过alter table add partition方式增加Hive的分区的&#xff0c;但有时候会通过HDFS put/cp命令或flink、flum程序往表目录下拷贝分区目录&#xff0c;如果目录多&#xff0c;需要执行多条alter语句&#xff0c;非常麻烦。Hive提供了一个"…

【学生管理系统】权限管理之用户管理

目录 6. 权限管理 6.1 环境搭建 6.1.1 数据库 6.1.2 后端环境 6.2 用户管理 6.2.1 查询所有用户&#xff08;关联角色&#xff09; 6.2.2 核心1&#xff1a;给用户授予角色 6. 权限管理 6.1 环境搭建 6.1.1 数据库 权限管理的5张表的关系 添加4张表 # 权限表&…

PTPVT 插值说明

文章目录 PTPVT 插值说明 PTPVT 插值说明PVT Hermite插值PVT 三次多项式插值PT 插值Sin轨迹测试结果PVT Hermite插值结果PVT 三次多项式插值结果PT 插值结果 用户轨迹测试结果PVT Hermite插值结果PT 插值结果 PTPVT 插值说明 PT模式&#xff1a; 位置-时间路径插值算法。 PVT模…

EMNLP'24 最佳论文解读 | 大语言模型的预训练数据检测:基于散度的校准方法

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 点击 阅读原文 观看作者讲解回放&#xff01; 作者简介 张伟超&#xff0c;中国科学院计算所网络数据科学与技术重点实验室三年级直博生 内容简介 近年来&#xff0c;大语言模型&#xff08;LLMs&#xff09;的…

[Day 10]有序数组的平方

今天这道题较之前的思路会好理解些&#xff0c;题目链接&#xff1a;977.有序数组的平方 题目描述&#xff1a; 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示例 1&#xff1a; 输入&…

大O算法的魔法世界

引言 嘿&#xff0c;小朋友们&#xff0c;今天我们要一起探索一个神秘的魔法世界——大O算法。这听起来可能有点奇怪&#xff0c;但它其实是一种帮助我们理解计算机程序运行速度的方式。想象一下&#xff0c;我们有很多不同的魔法咒语&#xff08;算法&#xff09;&#xff0c…

UE(虚幻)学习(四) 第一个C++类来控制小球移动来理解蓝图和脚本如何工作

UE5视频看了不少&#xff0c;但基本都是蓝图如何搞&#xff0c;或者改一下属性&#xff0c;理解UE系统现有组件使用的。一直对C脚本和蓝图之间的关系不是很理解&#xff0c;看到一个视频讲的很好&#xff0c;我也做笔记记录一下。 我的环境是UE5.3.2. 创建UE空项目 我们创建…

细说STM32F407单片机IIC总线基础知识

目录 一、 I2C总线结构 1、I2C总线的特点 2、I2C总线通信协议 3、 STM32F407的I2C接口 二、 I2C的HAL驱动程序 1、 I2C接口的初始化 2、阻塞式数据传输 &#xff08;1&#xff09;函数HAL_I2C_IsDeviceReady() &#xff08;2&#xff09;主设备发送和接收数据 &#…

关于easy-es对时间范围查询遇到的小bug

前言&#xff1a;在使用easy-es之前作为一个小白的我只有es原生查询的基础&#xff0c;在自己通过查看官方文档自学easy-es遇到了一个挫折&#xff0c;其他的还好语法和MybatisPlus差不多&#xff0c;正以为我觉得很快就能入手&#xff0c;在对时间范围的判断就给我当头一棒&am…