【实用工具】SpringBoot实现接口签名验证

需求场景

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

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

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

项目路径

https://gitee.com/charles_ruan/easy-sign

代码实现

不同的客户端有着不同的appSecret

  • 通过密钥可以为不同的客户端(调用方) 分配不同的appSecret,来区分不同客户端app(调用方)。
  • 将获取到的appSecret 参与到sign(签名)的生成,保证了客户端的请求签名是由我们后台控制的。

定义切面,拦截带SignatureValidation方法。

  • 获取方法上的参数,存入SortedMap
  • 判断参数是否合法
    • 判断appId是否存在对应的secret
    • 判断时间戳是否有效
  • 进行签名校验
@Slf4j
@Aspect
public class SignAspect {

    @Before("@annotation(signatureValidation)")
    public void doBefore(SignatureValidation signatureValidation) throws Throwable {
        // 接收到请求,记录请求内容
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        SortedMap<String, String> allParams = HttpUtils.getAllParams(request);

        // 1、获取请求sign签名参数,
        String sign = allParams.get("sign");
        if (StrUtil.isBlank(sign)) {
            throw new RuntimeException("sign不能为空");
        }
        // 2、获取请求参数secret
        String appId = allParams.get("appId");
        String appSecret = getAppSecret(appId);
        if (StrUtil.isBlank(appSecret)) {
            throw new RuntimeException("appId不合法");
        }
        // 3、获取请求参数timestamp 时间戳,
        String timestamp = allParams.get("timestamp");
        if (StrUtil.isBlank(timestamp)) {
            throw new RuntimeException("timestamp不能为空");
        }
        //3. 比较时间,120s内为合法请求
        if (Math.abs(Long.parseLong(timestamp) - System.currentTimeMillis()) > 120000) {
            throw new RuntimeException("timestamp失效");
        }
        allParams.put("secret", appSecret);
        verifySign(allParams);
    }
    
    private void verifySign(SortedMap<String, String> allParams) {
        // 对方签名
        String sign = allParams.get("sign");
        allParams.remove("sign");
        String mySign = SecureUtil.md5(JSONUtil.toJsonStr(allParams)).toUpperCase();

        log.info("验签,对方签名:{},我方签名:{}", sign, mySign);
        // 验签
        Assert.isTrue(StrUtil.equals(sign, mySign), "验签失败");
    }

    public String getAppSecret(String appId) {
        Map<String, String> map = new HashMap<>();

        map.put("zs001", "asd123fhg3b7fgh7dfg");
        map.put("ls001", "hghfgh123btgfyh1212");

        return map.get(appId);
    }
}

利用nonce参数,可以防止重复提交,在签名验证成功后,判断是否重复提交,原理就是结合redis,判断是否已经提交过

    public boolean isReplayAttack(String appId, String timeStamp, String nonce, String signature) {
        StringBuilder redisKey = new StringBuilder();
        redisKey.append("IS_REPLAY_ATTACK").append(":")
                .append(Constant.APP_ID).append(":").append(appId)
                .append(Constant.TIME_STAMP).append(":").append(timeStamp)
                .append(Constant.NONCE).append(":").append(nonce)
                .append(Constant.SIGN).append(":").append(signature);

        Object value = redisTemplate.opsForValue().get(redisKey);

        if (value != null && StringUtils.equals(signature, value.toString()))
            return false;
        else
            redisTemplate.opsForValue().set(redisKey, signature, 1000 * 50);
        return false;
    }
标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符 标题复制10行,并且每行大于10个字符

你可能感兴趣的:(实用工具,spring,boot,java,后端)