基于Spring Boot以及Redis使用Aop来实现Api接口签名验证

由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。

Api接口签名验证主要防御措施为以下几个:

请求发起时间得在限制范围内
请求的用户是否真实存在
是否存在重复请求
请求参数是否被篡改

实现思路:

我们按照主要防御措施先后顺序来实现,首先已知我们得到以下四个参数:
// 供应商的id,验证用户的真实性
String appid = request.getHeader("appid");
// 请求发起的时间
String timestamp = request.getHeader("timestamp");
// 随机数
String nonce = request.getHeader("nonce");
// 签名算法生成的签名
String sign = request.getHeader("sign");

请求发起时间得在限制范围内

像这种比较简单,就是获取服务器的当前时间去跟请求发起时间比较。
// 限制为(含)60秒以内发送的请求
long time = 60;
long now = System.currentTimeMillis() / 1000;
if (now - Long.valueOf(timestamp) > time) {
    return ObjectResponse.fail("请求发起时间超过服务器限制时间");
}

请求的用户是否真实存在

一般会有以下两个场景
场景一:在前后端分离的模式中,用户登录后得到token,用户调用接口时传递token来确保用户的真实性。
场景二:接口调用方不需要登录,那么我们接口提供方可以提供appid(调用时需要传递)与secret(在签名算法中使用)给接口调用方来验证用户的真实性。
这里我主要说一下场景二,如下:
// 查询appid是否正确来验证用户的真实性
CoreApiKey apiKey = coreApiKeyService.selectByAppid(appid);
if (apiKey == null) {
    return ObjectResponse.fail("appid参数错误");
}

是否存在重复请求

这里利用nonce参数,每次请求时先判断nonce在redis是否存在,存在则认为是重复请求,不存在就存放到redis中。但是这会有一个问题,随着请求的 		      次数越来越多,那么redis存放的nonce集合会越来越大,这肯定不是我们所期望的。这时我们可以巧妙的利用在请求发起时间得在限制范围内中的time(服务器限制60秒以内发生的请求),因为此步骤主要是验证请求是否重复,如果timestamp时间戳变了,那就不是重复请求了,所以我们可以在nonce存放到redis时给它设置一个过期时间(60秒),这样既保证了nonce的唯一性也不会发生nonce集合的无限大。
// 验证请求是否重复
if (redisService.hasKeyHashItem("third_party_key", apiKey.getAppid() + nonce)) {
    return ObjectResponse.fail("请不要发送重复的请求");
} else {
    // 如果nonce没有存在缓存中,则加入,并设置失效时间(秒)
    redisService.setHashItem("third_party_key", apiKey.getAppid() + nonce, nonce, time);
}

请求参数是否被篡改

终于到了最后环节,利用签名算法来生成签名。主要就是接口调用方的签名算法必须与接口提供方的签名算法一致。签名算法可以自己捣鼓捣鼓,我这里是先对key进行字典序排序,然后以url的参数格式进行拼接(secret在最后拼接),最后进行md5加密,以下一个Api接口签名验证就大功告成啦!
JSONObject signObj = new JSONObject();
signObj.put("appid", appid);
signObj.put("timestamp", timestamp);
signObj.put("nonce", nonce);
String mySign = getSign(signObj, apiKey.getSecret());
// 验证签名
if (!mySign.equals(sign)) {
    return ObjectResponse.fail("签名信息错误");
}

/**
 * 获取签名信息
 * @param data
 * @param secret
 * @return
 */
private static String getSign(JSONObject data, String secret) {
    // 由于map是无序的,这里主要是对key进行排序(字典序)
    Set<String> keySet = data.keySet();
    String[] keyArr = keySet.toArray(new String[keySet.size()]);
    Arrays.sort(keyArr);
    StringBuilder sbd = new StringBuilder();
    for (String k : keyArr) {
        if (StringUtil.isNotEmpty(data.getString(k))) {
            sbd.append(k + "=" + data.getString(k) + "&");
        }
    }
    // secret最后拼接
    sbd.append("secret=").append(secret);
    return MD5Util.encode(sbd.toString());
}

最后给大家一份基于SringBoot以及Redis使用Aop来实现Api接口签名验证的源码

@Component
@Aspect
@Slf4j
public class ThridPartyApiAspect {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HttpServletResponse response;

    @Autowired
    private RedisService redisService;

    @Autowired
    private CoreApiKeyService coreApiKeyService;

    /**
     * 表示匹配带有自定义注解的方法
     */
    @Pointcut("@annotation(com.stan.framework.anno.ThridPartyApi)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) {
        try {
            // 供应商的id,验证用户的真实性
            String appid = request.getHeader("appid");
            // 请求发起的时间
            String timestamp = request.getHeader("timestamp");
            // 随机数
            String nonce = request.getHeader("nonce");
            // 签名算法生成的签名
            String sign = request.getHeader("sign");
            if (StringUtil.isEmpty(appid) || StringUtil.isEmpty(timestamp) || StringUtil.isEmpty(nonce) || StringUtil.isEmpty(sign)) {
                return ObjectResponse.fail("请求头参数不能为空");
            }
            // 限制为(含)60秒以内发送的请求
            long time = 60;
            long now = System.currentTimeMillis() / 1000;
            if (now - Long.valueOf(timestamp) > time) {
                return ObjectResponse.fail("请求发起时间超过服务器限制时间");
            }
            // 查询appid是否正确
            CoreApiKey apiKey = coreApiKeyService.selectByAppid(appid);
            if (apiKey == null) {
                return ObjectResponse.fail("appid参数错误");
            }
            // 验证请求是否重复
            if (redisService.hasKeyHashItem("third_party_key", apiKey.getAppid() + nonce)) {
                return ObjectResponse.fail("请不要发送重复的请求");
            } else {
                // 如果nonce没有存在缓存中,则加入,并设置失效时间(秒)
                redisService.setHashItem("third_party_key", apiKey.getAppid() + nonce, nonce, time);
            }
            JSONObject signObj = new JSONObject();
            signObj.put("appid", appid);
            signObj.put("timestamp", timestamp);
            signObj.put("nonce", nonce);
            String mySign = getSign(signObj, apiKey.getSecret());
            // 验证签名
            if (!mySign.equals(sign)) {
                return ObjectResponse.fail("签名信息错误");
            }
            try {
                return point.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
            return ObjectResponse.fail("解析请求参数异常");
        }
        return null;
    }

    /**
     * 获取签名信息
     * @param data
     * @param secret
     * @return
     */
    private static String getSign(JSONObject data, String secret) {
        // 由于map是无序的,这里主要是对key进行排序(字典序)
        Set<String> keySet = data.keySet();
        String[] keyArr = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArr);
        StringBuilder sbd = new StringBuilder();
        for (String k : keyArr) {
            if (StringUtil.isNotEmpty(data.getString(k))) {
                sbd.append(k + "=" + data.getString(k) + "&");
            }
        }
        // secret最后拼接
        sbd.append("secret=").append(secret);
        return MD5Util.encode(sbd.toString());
    }
}

我不去想是否能够成功
既然选择了远方
便只顾风雨

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值