• 【SpringBoot】之接口设计防篡改和防重放攻击


    目录


    一、API 接口防篡改方案


    1、API 接口暴露问题

    由于提供给第三方服务调用的 API 接口需要暴露在外网中,并且接口上提供了具体的请求地址和请求参数,那么,接口就有可能被人抓包拦截并对请求参数进行修改后再次发起请求,这样一来可能会被盗取信息,二来服务器可能会受到攻击。

    为了防止这种情况发生,需要采取安全机制措施进行防范,方法有多种,比如:

    • 接口采用 https 的传输方式,https 传输的数据是经过了加密的,可以保证不被篡改(关于 https 的知识可以参考我的另一篇博客:【计算机网络】之 HTTPS 保证数据安全防止被篡改);
    • 项目后台采用安全的验证机制,比如采用参数加密请求时间限制来防止参数篡改和二次投放(我们以这种方式为案例进行讲解)。

    2、防止接口参数篡改

    为了防止参数被抓包篡改参数,我们可以对参数进行加密。具体方式如下:

    • 前端使用约定好的秘钥对传输参数进行加密,得到签名值 sign1,并且将签名值存入headers,然后发送请求给服务端;
    • 服务端接收客户端的请求后,在过滤器中使用约定好的秘钥对请求的参数再次进行签名,得到签名值 sign2
    • 最后对比 sign1 和 sign2 的值,如果相同,则认定为合法请求,如果不同,则说明参数被篡改,认定为非法请求。

    3、防止接口重投放

    重投放或者叫二次投放,指的是接口被人拦截篡改参数后重新发送请求。防止重投放的方案是:基于 timestamp 对参数进行签名,具体的实现是:

    • 每次 http 请求,headers 都加上 timestamp 时间戳,并且 timestamp 和请求的参数一起进行数字签名;
    • 服务器收到请求之后,先判断时间戳参数与当前时间是否超过了60s(这个可以自定义配置,一般黑客从抓包到重放的耗时远远超过了60s),如果超过了则提示签名过期;
    • 如果黑客修改了 timestamp 参数,则 sign 参数对应的数字签名就会失效,因为黑客不知道签名秘钥,没有办法生成新的数字签名(前端一定要保护好秘钥和加密算法)。

    二、核心思路代码设计


    代码思路主要是通过创建过滤器,对参数进行签名验证,核心过滤器代码如下:

    @Component
    @Slf4j
    public class SignAuthFilter implements Filter {
    
        @Autowired
        private SignAuthProperties signAuthProperties;
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            log.info("初始化 SignAuthFilter...");
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    
            HttpServletRequest httpRequest = (HttpServletRequest) request;
    
            // 过滤不需要签名验证的地址
            String requestUri = httpRequest.getRequestURI();
            for (String ignoreUri : signAuthProperties.getIgnoreUri()) {
                if (requestUri.contains(ignoreUri)) {
                    log.info("当前URI地址:" + requestUri + ",不需要签名校验!");
                    chain.doFilter(request, response);
                    return;
                }
            }
    
            // 获取签名和时间戳
            String sign = httpRequest.getHeader("Sign");
            String timestampStr = httpRequest.getHeader("Timestamp");
            if (StringUtils.isEmpty(sign)) {
                responseFail("签名不能为空", response);
                return;
            }
            if (StringUtils.isEmpty(timestampStr)) {
                responseFail("时间戳不能为空", response);
                return;
            }
    
            // 重放时间限制
            long timestamp = Long.parseLong(timestampStr);
            if (System.currentTimeMillis() - timestamp >= signAuthProperties.getTimeout()*1000) {
                responseFail("签名已过期", response);
                return;
            }
    
            // 校验签名
            Map<String, String[]> parameterMap = httpRequest.getParameterMap();
            if (SignUtils.verifySign(parameterMap, sign, timestamp)) {
                chain.doFilter(httpRequest, response);
            } else {
                responseFail("签名校验失败", response);
            }
    
        }
    
        /**
         * 响应错误信息
         */
        private void responseFail(String msg, ServletResponse response) throws IOException {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter out = response.getWriter();
            out.println(msg);
            out.flush();
            out.close();
        }
    
        @Override
        public void destroy() {
            log.info("销毁 SignAuthFilter...");
        }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    这里我们可以在 yml 配置文件中进行配置重放超时时间和不过滤的 URI 地址:

    sign:
      # 签名超时时间
      timeout: 60
      # 允许未签名访问的 url 地址
      ignoreUri:
        - /swagger-ui.html
        - /v2/api-docs
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后编写配置类:

    @Data
    @ConfigurationProperties(prefix = "sign")
    public class SignAuthProperties {
    
        /**
         * 签名超时时间
         */
        private Integer timeout;
    
        /**
         * 允许未签名访问的 url 地址
         */
        private List<String> ignoreUri;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在签名的校验代码中,省略了具体的校验方法,因为不同的加密方式对应不用的校验方法,所以这里只提供一个代码思路,只要前后端约定好如何进行加密即可

  • 相关阅读:
    大数据ClickHouse进阶(十三):ClickHouse的GROUP BY 子句
    [附源码]Python计算机毕业设计SSM篮球资讯网站(程序+LW)
    P1068 [NOIP2009 普及组] 分数线划定
    go get 拉取报错The project you were looking for could not be found的解决方法
    Pycharm连接远程服务器
    541.反转字符串
    八股文之jdk源码分析
    最好的开放式蓝牙耳机有哪些?排名前五的开放式耳机五强
    Scala爬虫实战:采集网易云音乐热门歌单数据
    thinkphp5.0.24反序列化漏洞分析
  • 原文地址:https://blog.csdn.net/aiwangtingyun/article/details/126640870