三方开放接口,Springboot通过AOP实现API接口的签名验证

前言

对外开放的接口,需要验证请求方发送过来的数据确实是由发送方发起的,并且中途不能被篡改和伪造,所以才会对接口的访问进行签名验证,以保证双方获取到的原来的信息是没有经过篡改的。

实现方法

对请求的信息内容,通过MD5运算或者其他算法(必须是不可逆的签名算法)生成签名标识,后端拿到请求的信息内容,经过同样的算法得到签名标识,比对和接收到的签名一致,则验证为真

签名规则

这里我自己只用到了timestamp加入签名,实际开发种可能会用到appId、pwd等参数,按实际需求加入到规则中即可,签名规则根据实际变更,此处只作参考

1. 所有的参数按参数名升序排序
2. 按参数名及参数值互相连接组成一个请求参数串(paramStr),格式如下:
body内容#createName=default#createUser=10000#reportUser=张三#state=0#
注:参数有3种,body直接在后面拼接#,前2种按参数名及参数值互相连接,拼接顺序:body#queryParams#pathUrl#,每个参数后面以#结束
        ①URL占位符中的参数
        ②QueryParams参数,普通请求参数
        ③body参数
3. 将secretKey(服务器密钥:ee4xxxxxxxxxxxx3e)和timestamp,拼接到请求参数串的头部,得到签名字符串(signStr)
secretKey="+secretKey#timestamp="+timestamp+"#paramStr+"
4. 将signStr通过MD5加密,得到签名字符串(sign)
5. timestamp 、sign 放到 Headers中,与其他接口请求参数一起发送给服务端

步骤1:自定义签名验证注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME )
public @interface CheckSign {
}

步骤2:自定义验证签名切面AOP,实现签名验证

@Aspect   //定义一个切面
@Configuration
@Slf4j
public class CheckSignAspect {

    @Value("${sign.expireTime}")
    private long expireTime;//接口签名验证超时时间
    @Value("${sign.secretKey}")
    private String secretKey;//接口签名唯一密钥

    // 定义切点Pointcut
    @Pointcut("@annotation(com.jxzx.verify.common.annotation.CheckSign)")
    public void excudeService() {
    }

    @Around("excudeService()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        log.info("开始验证签名");
        try {
            ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = Objects.requireNonNull(sra).getRequest();

            String timestamp = request.getHeader("timestamp");//获取timestamp参数
            String sign = request.getHeader("sign");//获取sign参数

            if (StrUtil.isBlank(timestamp) || StrUtil.isBlank(sign)) {
                return RestResult.failed("timestamp和sign参数不能为空");
            }
            long requestTime = Long.valueOf(timestamp);
            long now = System.currentTimeMillis() / 1000;
            log.info("now={}", now);
            // 请求发起时间与当前时间超过expireTime,则接口请求过期
            if (now - requestTime > expireTime) {
                return RestResult.failed("接口请求过期");
            }

            String generatedSign = generatedSignature(request, timestamp);
            if (!generatedSign.equals(sign)) {
                return RestResult.failed("签名校验错误");
            }

            Object result = joinPoint.proceed();
            return result;
        } catch (Throwable t) {
            return RestResult.failed("签名校验异常");
        }

    }
    
    //获取请求参数并生成签名
    private String generatedSignature(HttpServletRequest request, String timestamp) {
        //获取RequestBody参数,此处需要配合过滤器处理request后才能获取
        String bodyParam = null;
        if (request instanceof ContentCachingRequestWrapper) {
            bodyParam = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), StandardCharsets.UTF_8);
        }

        //获取RequestParam参数
        Map<String, String[]> requestParameterMap = request.getParameterMap();

        //获取PathVariable参数
        ServletWebRequest webRequest = new ServletWebRequest(request, null);
        Map<String, String> requestPathMap = (Map<String, String>) webRequest.getAttribute(
                HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);

        return SignUtil.sign(bodyParam, requestParameterMap, requestPathMap, secretKey, timestamp);
    }

}

注意:

获取RequestBody参数,@RequestBody读取参数主要是通过request中的ServletInputStream传输,SpirngMvc通过@RequestBody读取流中的数据封装到对象中。因为stream只能被读取一次,如果这里我们通过request读取,后面的SpringMvc就读取不到了,因此通过Filter对request进行处理。

新增一个过滤器
public class RequestCachingFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
        boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestWrapper = request;
        if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
            requestWrapper = new ContentCachingRequestWrapper(request);
        }
        try {
            filterChain.doFilter(requestWrapper, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
配置过滤器
@Configuration
public class FilterConfig {
    @Bean
    public RequestCachingFilter requestCachingFilter() {
        return new RequestCachingFilter();
    }

    @Bean
    public FilterRegistrationBean requestCachingFilterRegistration(
            RequestCachingFilter requestCachingFilter) {
        FilterRegistrationBean bean = new FilterRegistrationBean(requestCachingFilter);
        bean.setOrder(1);
        return bean;
    }
}
生成签名的工具类
public class SignUtil {

    /**
     * 使用 Map按key进行排序
     *
     * @param map
     * @return
     */
    public static Map<String, String> sortMapByKey(Map<String, String> map) {
        if (map == null || map.isEmpty()) {
            return null;
        }
        //升序排序
        Map<String, String> sortMap = new TreeMap<>(String::compareTo);
        sortMap.putAll(map);
        return sortMap;
    }

    public static String sign(String body, Map<String, String[]> params, Map<String, String> requestPathMap, String secretKey, String timestamp) {
        StringBuilder sb = new StringBuilder();
        if (CharSequenceUtil.isNotBlank(body)) {
            sb.append(body).append('#');
        }

        if (!CollectionUtils.isEmpty(params)) {
            params.entrySet()
                    .stream()
                    .sorted(Map.Entry.comparingByKey())
                    .forEach(paramEntry -> {
                        String paramValue = String.join(",", Arrays.stream(paramEntry.getValue()).sorted().toArray(String[]::new));
                        sb.append(paramEntry.getKey()).append("=").append(paramValue).append('#');
                    });
        }

        if (ArrayUtil.isNotEmpty(requestPathMap)) {
            for (String key : requestPathMap.keySet()) {
                String value = requestPathMap.get(key);
                sb.append(key).append("=").append(value).append('#');
            }

        }


        return SecureUtil.md5(String.join("#", secretKey, timestamp, sb.toString()));
    }
}

步骤3:使用

在我们需要验证签名的controller上加上 @CheckSign 即可

    @ApiOperation(value = "验证签名")
    @PostMapping("/checkSign")
    @CheckSign
    public RestResult postTestPdf(
            @PathVariable("name") String name,
            @PathVariable("age") String age,
            @ApiParam(value = "搜索条件", required = true) String orderId,
            @ApiParam(value = "搜索条件1", required = true) String orderNo,
            @RequestBody(required = true) @Validated JSONArray jsonArray
    ) {

        return RestResult.success(jsonArray);
    }

  • 1
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值