关于AOP实现API接口签名校验

1. 签名的概念

目的: 为了确认某个信息确实是由某个发送方发送的,或者某个发布内容确实是由发送方发布的,任何人都不可能伪造消息,并且,发送方也不能抵赖。
方法: 对发布的信息内容,通过某种可靠的加工(比如进行MD5运算),生成签名标识(字符串序列或者证书之类)
验证: 任何人拿到发布的信息内容后,可以通过同样的加工,得出签名标识,如果比对和发布者公布的签名一致,则验证为真。
签名与加密区别: 加密是为了不让别人知道原来的信息,签名是为了保证大家获取到的原来的信息是没有经过改动的。

2. Web API使用安全签名的实现

我们提及了几个参数,一个是加密签名字符串,一个是时间戳,一个是随机数,一个是应用接入ID,我们一般的处理规则如下所示。

  1. Web API 为各种应用接入,如APP、Web、Winform等接入端分配应用AppID以及通信密钥AppSecret,双方各自存储。
  2. 接入端在请求Web API接口时需携带以下参数:signature、 timestamp、nonce、appid,签名是根据几个参数和加密秘钥生成。
  3. Web API 收到接口调用请求时需先检查传递的签名是否合法,验证后才调用相关接口。

加密签名在服务端(Web API端)的验证流程参考微信的接口的处理方式,处理逻辑如下所示。

  1. 检查timestamp 与系统时间是否相差在合理时间内,如10分钟。
  2. 将所有参数进行字典序排序
  3. 将三个参数字符串拼接成一个字符串进行MD5加密
  4. 加密后的字符串可与signature对比,若匹配则标识该次请求来源于某应用端,请求是合法的。

3. Java实现

定义一个实体类,用来做请求基础参数

@Data
@ToString
public class BaseReq {

    // 签名
    private String signature;

    // 时间戳
    private String timestamp;

    // appid
    private String appId;

    // 随机数
    private String nonce;

}

使用AOP进行参数校验,如果参数按加密规则加密后等于签名,则认为参数没有被必改过

@Slf4j
@Aspect
@Configuration
public class SignCheckApsect {

    @Value("${api.appSecret}")
    private String appSecret;

    @Around("execution(* com.yuxiang.user.api.controller.*.*(..))")
    public Object doAroundService(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 反射获取方法参数
        Object[] args = proceedingJoinPoint.getArgs();
        Map<String,Object> map = new HashMap<>();
        for (Object arg : args) {
            Class<?> clazz = arg.getClass();
            while (clazz != null){
                for (Field field : clazz.getDeclaredFields()) {
                    field.setAccessible(true);
                    if (Objects.isNull(field.get(arg))) {
                        continue;
                    }
                    map.put(field.getName(),field.get(arg));
                }
                clazz = clazz.getSuperclass();
            }
        }
        try {
            // 移除signature,签名不参与排序加密
            Object signature = map.remove("signature");

            //校验时间戳
            checkSignTime(map.get("timestamp").toString());

            if (!signature.equals(sign(map, appSecret))) {
                throw new RuntimeException("签名校验失败,请检查签名加密方式");
            }
        }catch (Exception e){
            throw new AuthException(e.getMessage());
        }
        return proceedingJoinPoint.proceed();
    }

    /**
     * 生成签名
     * @param paramMap 参数列表
     * @param appSecret 密钥
     * @return 签名字符串
     */
    private String sign(Map<String, Object> paramMap, String appSecret) {
        List<String> paramList = new ArrayList<String>();
        for (String param : paramMap.keySet()) {
            paramList.add(param);
        }
        Collections.sort(paramList);
        StringBuffer signsb = new StringBuffer();
        for (String str : paramList) {
            if (paramMap.get(str) == null || paramMap.get(str).equals("")) {
                continue;
            }
            signsb.append("&" + str + "=" + paramMap.get(str));
        }
        signsb.append("&appSecret=" + appSecret);
        String signStr = DigestUtils.md5DigestAsHex(signsb.toString().substring(1).getBytes()).toUpperCase();
        return signStr;
    }

    /**
     * 时间戳校验
     * @param timestamp 时间戳 前后10分钟
     * @throws Throwable
     */
    private void checkSignTime(String timestamp) throws Throwable {
        long currentTime = System.currentTimeMillis();
        long requestTime = 0;
        try {
            requestTime = Long.parseLong(timestamp);
        } catch (NumberFormatException e) {
            throw new BaseException("未找到时间戳");
        }
        if (Math.abs(currentTime - requestTime) > 600000) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            throw new BaseException(String.format("请求已过期,服务器当前时间:%s", simpleDateFormat.format(currentTime)));
        }
    }

}


4. 测试签名

编写一个Controller

@RestController
@RequestMapping("/v1/callback/")
public class CallBackApi {

    @PostMapping("/test")
    public String Test(@RequestBody BaseReq baseReq){
       return "签名校验通过";
    }
}

发送请求

{
    "timestamp":"1626414311357",
    "appId":"1001",
    "nonce":"123456",
    "signature":"9479879AF939F1B81F80C1B110861B5E"
}

测试结果
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值