Spring Boot接口设计防篡改、防重放攻击详解

本示例主要内容

  • 请求参数防止篡改攻击
  • 基于timestamp方案,防止重放攻击
  • 使用swagger接口文档自动生成

API接口设计

API接口由于需要供第三方服务调用,所以必须暴露到外网,并提供了具体请求地址和请求参数,为了防止被别有用心之人获取到真实请求参数后再次发起请求获取信息,需要采取很多安全机制。

  • 需要采用https方式对第三方提供接口,数据的加密传输会更安全,即便是被破解,也需要耗费更多时间
  • 需要有安全的后台验证机制,达到防参数篡改+防二次请求(本示例内容)

防止重放攻击必须要保证请求只在限定的时间内有效,需要通过在请求体中携带当前请求的唯一标识,并且进行签名防止被篡改,所以防止重放攻击需要建立在防止签名被串改的基础之上

防止篡改

  • 客户端使用约定好的秘钥对传输参数进行加密,得到签名值sign1,并且将签名值存入headers,发送请求给服务端
  • 服务端接收客户端的请求,通过过滤器使用约定好的秘钥对请求的参数(headers除外)再次进行签名,得到签名值sign2。
  • 服务端对比sign1和sign2的值,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,认定为非法请求

基于timestamp的方案,防止重放

每次HTTP请求,headers都需要加上timestamp参数,并且timestamp和请求的参数一起进行数字签名。因为一次正常的HTTP请求,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间相比较,是否超过了60s,如果超过了则提示签名过期(这个过期时间最好做成配置)。

一般情况下,黑客从抓包重放请求耗时远远超过了60s,所以此时请求中的timestamp参数已经失效了。

如果黑客修改timestamp参数为当前的时间戳,则sign参数对应的数字签名就会失效,因为黑客不知道签名秘钥,没有办法生成新的数字签名(前端一定要保护好秘钥和加密算法)。

相关核心思路代码

过滤器

@Slf4j
@Component
/**
 * 防篡改、防重放攻击过滤器
 */
public class SignAuthFilter implements Filter {
  @Autowired
  private SecurityProperties securityProperties;

  @Override
  public void init(FilterConfig filterConfig) {
    log.info("初始化 SignAuthFilter");
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 防止流读取一次后就没有了, 所以需要将流继续写出去
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletRequest requestWrapper = new RequestWrapper(httpRequest);

    Set<String> uriSet = new HashSet<>(securityProperties.getIgnoreSignUri());
    String requestUri = httpRequest.getRequestURI();
    boolean isMatch = false;
    for (String uri : uriSet) {
      isMatch = requestUri.contains(uri);
      if (isMatch) {
        break;
      }
    }
    log.info("当前请求的URI是==>{},isMatch==>{}", httpRequest.getRequestURI(), isMatch);
    if (isMatch) {
      filterChain.doFilter(requestWrapper, response);
      return;
    }

    String sign = requestWrapper.getHeader("Sign");
    Long timestamp = Convert.toLong(requestWrapper.getHeader("Timestamp"));

    if (StrUtil.isEmpty(sign)) {
      returnFail("签名不允许为空", response);
      return;
    }

    if (timestamp == null) {
      returnFail("时间戳不允许为空", response);
      return;
    }

    //重放时间限制(单位分)
    Long difference = DateUtil.between(DateUtil.date(), DateUtil.date(timestamp * 1000), DateUnit.MINUTE);
    if (difference > securityProperties.getSignTimeout()) {
      returnFail("已过期的签名", response);
      log.info("前端时间戳:{},服务端时间戳:{}", DateUtil.date(timestamp * 1000), DateUtil.date());
      return;
    }

    boolean accept = true;
    SortedMap<String, String> paramMap;
    switch (requestWrapper.getMethod()) {
      case "GET":
        paramMap = HttpUtil.getUrlParams(requestWrapper);
        accept = SignUtil.verifySign(paramMap, sign, timestamp);
        break;
      case "POST":
      case "PUT":
      case "DELETE":
        paramMap = HttpUtil.getBodyParams(requestWrapper);
        accept = SignUtil.verifySign(paramMap, sign, timestamp);
        break;
      default:
        accept = true;
        break;
    }
    if (accept) {
      filterChain.doFilter(requestWrapper, response);
    } else {
      returnFail("签名验证不通过", response);
    }
  }

  private void returnFail(String msg, ServletResponse response) throws IOException {
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");
    PrintWriter out = response.getWriter();
    String result = JSONObject.toJSONString(AjaxResult.fail(msg));
    out.println(result);
    out.flush();
    out.close();
  }

  @Override
  public void destroy() {
    log.info("销毁 SignAuthFilter");
  }
}

签名验证

@Slf4j
public class SignUtil {

  /**
   * 验证签名
   *
   * @param params
   * @param sign
   * @return
   */
  public static boolean verifySign(SortedMap<String, String> params, String sign, Long timestamp) {
    String paramsJsonStr = "Timestamp" + timestamp + JSONObject.toJSONString(params);
    return verifySign(paramsJsonStr, sign);
  }

  /**
   * 验证签名
   *
   * @param params
   * @param sign
   * @return
   */
  public static boolean verifySign(String params, String sign) {
    log.info("Header Sign : {}", sign);
    if (StringUtils.isEmpty(params)) {
      return false;
    }
    log.info("Param : {}", params);
    String paramsSign = getParamsSign(params);
    log.info("Param Sign : {}", paramsSign);
    return sign.equals(paramsSign);
  }

  /**
   * @return 得到签名
   */
  public static String getParamsSign(String params) {
    return DigestUtils.md5DigestAsHex(params.getBytes()).toUpperCase();
  }
}

不做签名验证的接口做成配置(application.yml)

spring:
 security:
  # 签名验证超时时间
  signTimeout: 300
  # 允许未签名访问的url地址
  ignoreSignUri:
   - /swagger-ui.html
   - /swagger-resources
   - /v2/api-docs
   - /webjars/springfox-swagger-ui
   - /csrf

属性代码(SecurityProperties.java)

@Component
@ConfigurationProperties(prefix = "spring.security")
@Data
public class SecurityProperties {

  /**
   * 允许忽略签名地址
   */
  List<String> ignoreSignUri;

  /**
   * 签名超时时间(分)
   */
  Integer signTimeout;
}

签名测试控制器

@RestController
@Slf4j
@RequestMapping("/sign")
@Api(value = "签名controller", tags = {"签名测试接口"})
public class SignController {

  @ApiOperation("get测试")
  @ApiImplicitParams({
      @ApiImplicitParam(name = "username", value = "用户名", required = true, dataType = "String"),
      @ApiImplicitParam(name = "password", value = "密码", required = true, dataType = "String")
  })
  @GetMapping("/testGet")
  public AjaxResult testGet(String username, String password) {
    log.info("username:{},password:{}", username, password);
    return AjaxResult.success("GET参数检验成功");
  }

  @ApiOperation("post测试")
  @ApiImplicitParams({
      @ApiImplicitParam(name = "data", value = "测试实体", required = true, dataType = "TestVo")
  })
  @PostMapping("/testPost")
  public AjaxResult<TestVo> testPost(@Valid @RequestBody TestVo data) {
    return AjaxResult.success("POST参数检验成功", data);
  }

  @ApiOperation("put测试")
  @ApiImplicitParams({
      @ApiImplicitParam(name = "id", value = "编号", required = true, dataType = "Integer"),
      @ApiImplicitParam(name = "data", value = "测试实体", required = true, dataType = "TestVo")
  })
  @PutMapping("/testPut/{id}")
  public AjaxResult testPut(@PathVariable Integer id, @RequestBody TestVo data) {
    data.setId(id);
    return AjaxResult.success("PUT参数检验成功", data);
  }

  @ApiOperation("delete测试")
  @ApiImplicitParams({
      @ApiImplicitParam(name = "idList", value = "编号列表", required = true, dataType = "List<Integer> ")
  })
  @DeleteMapping("/testDelete")
  public AjaxResult testDelete(@RequestBody List<Integer> idList) {
    return AjaxResult.success("DELETE参数检验成功", idList);
  }
}

前端js请求示例

var settings = {
 "async": true,
 "crossDomain": true,
 "url": "http://localhost:8080/sign/testGet?username=abc&password=123",
 "method": "GET",
 "headers": {
  "Sign": "46B1990701BCF090E3E6E517751DB02F",
  "Timestamp": "1564126422",
  "User-Agent": "PostmanRuntime/7.15.2",
  "Accept": "*/*",
  "Cache-Control": "no-cache",
  "Postman-Token": "a9d10ef5-283b-4ed3-8856-72d4589fb61d,6e7fa816-000a-4b29-9882-56d6ae0f33fb",
  "Host": "localhost:8080",
  "Cookie": "SESSION=OWYyYzFmMDMtODkyOC00NDg5LTk4ZTYtODNhYzcwYjQ5Zjg2",
  "Accept-Encoding": "gzip, deflate",
  "Connection": "keep-alive",
  "cache-control": "no-cache"
 }
}

$.ajax(settings).done(function (response) {
 console.log(response);
});

注意事项

  • 该示例没有设置秘钥,只做了参数升排然后创建md5签名
  • 示例请求的参数md5原文本为:Timestamp1564126422{"password":"123","username":"abc"}
  • 注意headers请求头带上了Sign和Timestamp参数
  • js读取的Timestamp必须要在服务端获取
  • 该示例不包括分布试环境下,多台服务器时间同步问题

自动生成接口文档

配置代码

@Configuration
@EnableSwagger2
public class Swagger2Config {
  @Bean
  public Docket createRestApi() {
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .select()
        .apis(RequestHandlerSelectors.basePackage("com.easy.sign"))
        .paths(PathSelectors.any())
        .build();
  }

  //构建 api文档的详细信息函数,注意这里的注解引用的是哪个
  private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
        .title("签名示例")
        .contact(new Contact("签名示例网站", "http://www.baidu.com", "test@qq.com"))
        .version("1.0.0")
        .description("签名示例接口描述")
        .build();
  }
}

自动生成文档地址:http://localhost:8080/swagger-ui.html

资料

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • 详解如何在spring boot中使用spring security防止CSRF攻击

    CSRF是什么? CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF.  CSRF可以做什么? 你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求.CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成的问题包括:个人隐私泄露以及财产安全. CSRF漏洞现状 CSRF这种攻击方式

  • Spring boot进行参数校验的方法实例详解

    Spring boot开发web项目有时候我们需要对controller层传过来的参数进行一些基本的校验,比如非空.整数值的范围.字符串的长度.日期.邮箱等等.Spring支持JSR-303 Bean Validation API,可以方便的进行校验. 使用注解进行校验 先定义一个form的封装对象 class RequestForm { @Size(min = 1, max = 5) private String name; public String getName() { return n

  • spring boot 图片上传与显示功能实例详解

    首先描述一下问题,spring boot 使用的是内嵌的tomcat, 所以不清楚文件上传到哪里去了, 而且spring boot 把静态的文件全部在启动的时候都会加载到classpath的目录下的,所以上传的文件不知相对于应用目录在哪,也不知怎么写访问路径合适,对于新手的自己真的一头雾水. 后面想起了官方的例子,没想到一开始被自己找到的官方例子,后面太依赖百度谷歌了,结果发现只有官方的例子能帮上忙,而且帮上大忙,直接上密码的代码 package hello; import static org

  • Spring Boot的listener(监听器)简单使用实例详解

    监听器(Listener)的注册方法和 Servlet 一样,有两种方式:代码注册或者注解注册 1.代码注册方式 通过代码方式注入过滤器 @Bean public ServletListenerRegistrationBean servletListenerRegistrationBean(){ ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean

  • Spring Boot 中PageHelper 插件使用配置思路详解

    使用思路 1.引入myabtis和pagehelper依赖 2.yml中配置mybatis扫描和实体类 这2行代码 pageNum:当前第几页 pageSize:显示多少条数据 userList:数据库查询的数据数据列表 PageHelper.startPage(pageNum, pageSize); PageInfo pageInfo = new PageInfo(userList); 最后返回一个pageInfo 对象即可,pageInfo 这个对象中只有数据一些信息,但是,没有成功失败的状

  • Spring Boot 多数据源处理事务的思路详解

    目录 1. 思路梳理 2. 代码实践 2.1 案例准备 2.2 开始整活 LoadDataSource.java 3. 总结 首先我先声明一点,本文单纯就是技术探讨,要从实际应用中来说的话,我并不建议这样去玩分布式事务.也不建议这样去玩多数据源,毕竟分布式事务主要还是用在微服务场景下. 好啦,那就不废话了,开整. 1. 思路梳理 首先我们来梳理一下思路. 在上篇文章中,我们是一个微服务,在 A 中分别去调用 B 和 C,当 B 或者 C 有一个执行失败的时候,就去回滚.B 和 C 都是调用远程的

  • Spring Boot实现登录验证码功能的案例详解

    目录 验证码的作用 案例要求 前端页面准备 准备login.html页面 随机验证码工具类 后端控制器 验证码的作用 验证码的作用:可以有效防止其他人对某一个特定的注册用户用特定的程序暴力破解方式进行不断的登录尝试我们其实很经常看到,登录一些网站其实是需要验证码的,比如牛客,QQ等.使用验证码是现在很多网站通行的一种方式,这个问题是由计算机生成并且评判的,但是必须只有人类才能解答,因为计算机无法解答验证码的问题,所以回答出问题的用户就可以被认为是人类.验证码一般用来防止批量注册. 案例要求 验证

  • Spring Boot接口设计防篡改、防重放攻击详解

    本示例主要内容 请求参数防止篡改攻击 基于timestamp方案,防止重放攻击 使用swagger接口文档自动生成 API接口设计 API接口由于需要供第三方服务调用,所以必须暴露到外网,并提供了具体请求地址和请求参数,为了防止被别有用心之人获取到真实请求参数后再次发起请求获取信息,需要采取很多安全机制. 需要采用https方式对第三方提供接口,数据的加密传输会更安全,即便是被破解,也需要耗费更多时间 需要有安全的后台验证机制,达到防参数篡改+防二次请求(本示例内容) 防止重放攻击必须要保证请求

  • javascript防篡改对象实例详解

    本文实例讲述了javascript防篡改对象.分享给大家供大家参考,具体如下: JavaScript中对象内置有多个属性Configurable,Writable,Enumerable,Value,Get和Set,来控制属性的行为.同样的ES5也有几个方法,来指定对象的行为.我们知道,javascript中的对象是可以共享的,也是默认可拓展的: //一旦将对象设置防篡改,就不能撤销了 //众所周知,一般的对象是可以随意拓展的 var person = {name:'liufang'}; pers

  • spring boot如何基于JWT实现单点登录详解

    前言 最近我们组要给负责的一个管理系统 A 集成另外一个系统 B,为了让用户使用更加便捷,避免多个系统重复登录,希望能够达到这样的效果--用户只需登录一次就能够在这两个系统中进行操作.很明显这就是单点登录(Single Sign-On)达到的效果,正好可以明目张胆的学一波单点登录知识. 本篇主要内容如下: SSO 介绍 SSO 的几种实现方式对比 基于 JWT 的 spring boot 单点登录实战 注意: SSO 这个概念已经出现很久很久了,目前各种平台都有非常成熟的实现,比如OpenSSO

  • 利用spring boot如何快速启动一个web项目详解

    前言 基于我们创建好的lion项目,使用spring boot,我们就可以通过很少的一些配置,便可以启动这个项目.下面话不多说了,来一起看看详细的介绍吧. 方法如下: 1 引入Spring boot,我们打开lion父模块的pom文件,继承 spring boot的pom 2让lion-web模块依赖spring boot的web相关的jar包,打开lion-web项目下的pom文件,添加如下的依赖 3 添加spring boot入口启动类Application.java,这个类要房子lion-

随机推荐