小工具知识网

当前位置: 首页 > 行业知识 > 超详细!完整版!基于spring对外开放接口的签名认证方案(拦截器方式)

行业知识

超详细!完整版!基于spring对外开放接口的签名认证方案(拦截器方式)

2026-11-12 19:42:09 来源:互联网转载

文章目录

  • 1、场景
  • 2、接口防御措施
  • 3、签名认证逻辑
  • 4、签名算法规则
  • 5、代码示例
    • 1、sign工具类
    • 2、定义拦截器
    • 3、生成accessKey、secretKey 工具类
    • 4、signInterceptor类
    • 5、SignInterceptor 获取body里参数后,接口的controller会获取不到body的参数了,会报错

      1、场景

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

      2、接口防御措施

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

      3、签名认证逻辑

      1、服务端生成一对 accessKey/secretKey密钥对,将 accessKey公开给客户端,将 secretKey 保密。

      2、客户端使用 secretKey和一些请求参数(如时间戳、请求内容等),使用 MD5 算法生成签名。

      3、客户端将 accessKey、签名和请求参数一起发送给服务端。

      4、服务端使用 和收到的请求参数,使用 MD5 算法生成签名。

      5、服务端比较客户端发来的签名和自己生成的签名是否相同,如果相同,则认为请求是可信的,否则认为请求是不可信的。

      secretKey不进行网络传输,只用于本地MD5运算

      4、签名算法规则

      计算步骤

      用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。

      将请求参数对按key进行字典升序排序,得到有序的参数对列表N

      将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8

      将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)

      对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名

      注意事项

      不同接口要求的参数对不一样,计算签名使用的参数对也不一样

      参数名区分大小写,参数值为空不参与签名

      URL键值拼接过程value部分需要URL编码

      5、代码示例

      1、sign工具类

      public class SignUtil {    /**     * 签名算法     * 1. 计算步骤     * 用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。     * 将请求参数对按key进行字典升序排序,得到有序的参数对列表N     * 将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8     * 将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)     * 对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名     * 2. 注意事项     * 不同接口要求的参数对不一样,计算签名使用的参数对也不一样     * 参数名区分大小写,参数值为空不参与签名     * URL键值拼接过程value部分需要URL编码     * @return 签名字符串     */    private static String getSign(Map map, String secretKey) {        List infoIds = new ArrayList(map.entrySet());        Collections.sort(infoIds, new Comparator() {            public int compare(Map.Entry arg0, Map.Entry arg1) {                return (arg0.getKey()).compareTo(arg1.getKey());            }        });        StringBuffer sb = new StringBuffer();        for (Map.Entry m : infoIds) {            if(null == m.getValue() || StringUtils.isNotBlank(m.getValue().toString())){                sb.append(m.getKey()).append("=").append(URLUtil.encodeAll(m.getValue().toString())).append("&");            }        }        sb.append("secret-key=").append(secretKey);        return MD5.create().digestHex(sb.toString()).toUpperCase();    }    //获取随机值    private static String getNonceStr(int length){        //生成随机字符串        String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";        Random random=new Random();        StringBuffer randomStr=new StringBuffer();        // 设置生成字符串的长度,用于循环        for(int i=0; i            //从62个的数字或字母中选择            int number=random.nextInt(62);            //将产生的数字通过length次承载到sb中            randomStr.append(str.charAt(number));        }        return randomStr.toString();    }    //签名验证方法    public static boolean signValidate(Map        String mySign = getSign(map,secretKey);        return mySign.equals(sign);    }}    @Override    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(signInterceptor())                .addPathPatterns("/openapi/**");//只拦截openapi前缀的接口    }//交给spring管理 SignInterceptor bean //不然下边 private OpenApiApplyMapper applyMapper;注入为null    @Bean    public SignInterceptor signInterceptor(){        return new SignInterceptor();    }}    private static final int KEY_LENGTH = 32; // 指定生成的key长度为32字节    public static String generateAccessKey() {        SecureRandom random = new SecureRandom();        byte[] bytes = new byte[KEY_LENGTH / 2]; // 生成的字节数要除以2        random.nextBytes(bytes);        return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 20);    }    public static String generateSecretKey() {        SecureRandom random = new SecureRandom();        byte[] bytes = new byte[KEY_LENGTH];        random.nextBytes(bytes);        return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 40);    }}    private static final String ACCESSKEY = "access-key";//调用者身份唯一标识    private static final String TIMESTAMP = "time-stamp";//时间戳    private static final String SIGN = "sign";//签名    private static final String NONCE = "nonce";//随机值    @Resource    private OpenApiApplyMapper applyMapper;    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        if(checkSign(request, response)){//签名认证            return HandlerInterceptor.super.preHandle(request, response, handler);        }        return false;    }    /**     * 验证签名     * @param request     * @param response     * @return     * @throws Exception     */    private boolean checkSign(HttpServletRequest request,HttpServletResponse response)throws Exception {        response.setContentType("application/json");        response.setCharacterEncoding("utf8");        String ip = IPUtils.getIpAddr(request);        FzyLogUtil.infoSafe("开放接口", "访问时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL());        String accessKey = request.getHeader(ACCESSKEY);        String timestamp = request.getHeader(TIMESTAMP);        String nonce = request.getHeader(NONCE);        String sign = request.getHeader(SIGN);        if (!StringUtils.isNotBlank(accessKey)) {            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey无效")));            FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:accessKey无效");            return false;        }        if (StringUtils.isBlank(sign)) {            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("签名无效")));            FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:签名无效");            return false;        }        OpenApiDetailDO openApiDetailDO = applyMapper.selectOneByAccessKey(accessKey);        if (openApiDetailDO == null) {            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey不存在")));            FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:accessKey不存在");            return false;        }        if (StringUtils.isNotBlank(openApiDetailDO.getBlackList())) {            for (String bIp : openApiDetailDO.getBlackList().split(",")) {                if (bIp.equals(ip)) {//黑名单                    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求")));                    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:黑名单拒绝请求");                    return false;                }            }        }        if (StringUtils.isNotBlank(openApiDetailDO.getWhiteList())) {            boolean flag = false;            for (String bIp : openApiDetailDO.getWhiteList().split(",")) {                if (bIp.equals(ip)) {//白名单                    flag = true;                    break;                }            }            if(!flag){                response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求")));                FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:白名单未符合拒绝请求");                return false;            }        }        if ("0".equals(openApiDetailDO.getInvokeStatus() + "")) {            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("访问权限已被冻结")));            FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:访问权限已被冻结");            return false;        }        if (!"1".equals(openApiDetailDO.getApiStatus() + "")) {            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("接口异常,暂停访问")));            FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:接口异常,暂停访问");            return false;        }        if (!StringUtils.isNotBlank(timestamp)) {            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("时间戳无效")));            FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:时间戳无效");            return false;        } else if (openApiDetailDO.getTimeOut() != null) {            if (System.currentTimeMillis() - Long.valueOf(timestamp)  openApiDetailDO.getTimeOut() * 1000) {                response.getWriter().write(JSON.toJSONString(ResultUtil.fail("请求已过期")));                FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:请求已过期");                return false;            }            ;        }        Map hashMap = new HashMap();        String queryStrings = request.getQueryString();//获取url后边拼接的参数        if (queryStrings != null) {            for (String queryString : queryStrings.split("&")) {                String[] param = queryString.split("=");                if (param.length == 2) {                    hashMap.put(param[0], param[1]);                }            }        }        hashMap.put(ACCESSKEY, accessKey);        hashMap.put(TIMESTAMP, timestamp);        if (StringUtils.isNotBlank(nonce)) {            hashMap.put(NONCE, nonce);        }        String secretKey = openApiDetailDO.getSecretKey();        String body = new RequestWrapper(request).getBody();        if (StringUtils.isNotBlank(body)) {            Map map = JSON.parseObject(body);            if (map != null) {                hashMap.putAll(map);            }        }        if (!SignUtil.signValidate(hashMap, secretKey, sign)) {//认证失败            response.getWriter().write(JSON.toJSONString(ResultUtil.fail("认证失败")));            FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:认证失败");            return false;        }        return true;    }}

      5、SignInterceptor 获取body里参数后,接口的controller会获取不到body的参数了,会报错

      通过过滤器解决

      @Component@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")@Order(10000)public class HttpServletRequestFilter implements Filter {    @Override    public void init(FilterConfig filterConfig) throws ServletException {    }    @Override    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {        HttpServletRequest request = (HttpServletRequest) servletRequest;        String contentType = request.getContentType();        String method = "multipart/form-data";        if (contentType != null && contentType.contains(method)) {            // 将转化后的 request 放入过滤链中            request = new StandardServletMultipartResolver().resolveMultipart(request);        }        request = new RequestWrapper((HttpServletRequest) servletRequest);        //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中        // 在chain.doFiler方法中传递新的request对象        if(null == request) {            filterChain.doFilter(servletRequest, servletResponse);        } else {            filterChain.doFilter(request, servletResponse);        }    }    @Override    public void destroy() {    }}

上一篇:一文搞懂MQTT,如何在SpringBoot中使用MQTT实现消息的订阅和发布&MQTT 客户端重连

下一篇:ftp服务器怎么上传不了文件?