当前位置: 首页 > article >正文

五.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主要是希望可以灵活控制哪些接口是否需要验签,且灵活度较高,实际项目可根据需求自行选择


http://www.kler.cn/a/458438.html

相关文章:

  • Go IO之文件处理,TCPUDP讲解
  • CF2043b-B. Digits
  • ASP.NET Core Web API Hangfire
  • C# OpenCV机器视觉:漫水填充
  • 春招快速准备和是否考研建议
  • 深度学习实战102-基于深度学习的网络入侵检测系统,利用各种AI模型和pytorch框架实现网络入侵检测
  • STM32高级 以太网通讯案例1:网络搭建(register代码)
  • leetcode 面试经典 150 题:删除有序数组中的重复项
  • 基于SSM的“一汽租车辆共享平台”的设计与实现(源码+数据库+文档+PPT)
  • vue-复制剪贴板
  • pytorch整体环境打包安装到另一台电脑上
  • 高级技巧-使用Mysql 实现根据条件过滤整个分组数据
  • 正则化强度的倒数C——让模型学习更准确
  • Bash 脚本教程
  • 【Python】什么是元组(Tuple)?
  • TCP/IP原理
  • OpenCV-Python实战(4)——图像处理基础知识
  • 数据资产运营平台如何搭建?数据要素资产运营平台解决方案 解读
  • C++软件设计模式之代理(Proxy)模式
  • pikachu靶场搭建详细步骤