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();
}
}

资料

Spring Boot如何设计防篡改、防重放攻击接口的更多相关文章

  1. spring boot 框架设计步骤

    spring boot 框架设计步骤: 1.poem.xml配置 2.application.yml配置 3.entiry实体 4.realm.Myrealm extends AuthorizingR ...

  2. Spring Boot入门(四):开发Web Api接口常用注解总结

    本系列博客记录自己学习Spring Boot的历程,如帮助到你,不胜荣幸,如有错误,欢迎指正! 在程序员的日常工作中,Web开发应该是占比很重的一部分,至少我工作以来,开发的系统基本都是Web端访问的 ...

  3. Java | Spring Boot Swagger2 集成REST ful API 生成接口文档

      Spring Boot Swagger2 集成REST ful API 生成接口文档 原文 简介 由于Spring Boot 的特性,用来开发 REST ful 变得非常容易,并且结合 Swagg ...

  4. Spring Boot(九)Swagger2自动生成接口文档和Mock模拟数据

    一.简介 在当下这个前后端分离的技术趋势下,前端工程师过度依赖后端工程师的接口和数据,给开发带来了两大问题: 问题一.后端接口查看难:要怎么调用?参数怎么传递?有几个参数?参数都代表什么含义? 问题二 ...

  5. Spring boot 配置https 实现java通过https接口访问

    近来公司需要搭建一个https的服务器来调试接口(服务器用的spring boot框架),刚开始接触就是一顿百度,最后发现互联网认可的https安全链接的证书需要去CA认证机构申请,由于是调试阶段就采 ...

  6. Spring Boot 整合mybatis时遇到的mapper接口不能注入的问题

    现实情况是这样的,因为在练习spring boot整合mybatis,所以自己新建了个项目做测试,可是在idea里面mapper接口注入报错,后来百度查询了下,把idea的注入等级设置为了warnin ...

  7. Spring Boot 2.x基础教程:Swagger接口分类与各元素排序问题详解

    之前通过Spring Boot 2.x基础教程:使用Swagger2构建强大的API文档一文,我们学习了如何使用Swagger为Spring Boot项目自动生成API文档,有不少用户留言问了关于文档 ...

  8. 为spring boot 写的Controller中的rest接口配置swagger

    1.pom.xml文件中加入下列依赖: <dependency> <groupId>io.springfox</groupId> <artifactId> ...

  9. spring boot:用redis+lua实现表单接口的幂等性(spring boot 2.2.0)

    一,什么是幂等性? 1,幂等: 幂等操作:不管执行多少次,所产生的影响都和一次执行的影响相同. 幂等函数或幂等方法:可以使用相同的参数重复执行,并能获得相同的结果的函数/方法. 这些函数/方法不用担心 ...

随机推荐

  1. Java基础(三) String深度解析

    String可以说是Java中使用最多最频繁.最特殊的类,因为同时也是字面常量,而字面常量包括基本类型.String类型.空类型. 一. String的使用 1. String的不可变性 /** * ...

  2. SYN1618型 高精度天文时间同步系统

       SYN1618型 高精度天文时间同步系统 产品概述 SYN1618型 高精度天文时间同步系统是由西安同步电子科技有限公司精心设计.自行研发生产的一款高精度的时频频率标准设备,接收GPS.GLON ...

  3. 警惕SAP项目被“中间商赚差价”

    前段时间某买卖二手车的广告特别火,里面有一句话叫“没有中间商赚差价”特别有说服力.同样在做SAP项目的过程中也是要警惕各种“中间商”赚差价. 正常的SAP项目的都是甲方和乙方两边签署合同合作实施,并不 ...

  4. Netty源码分析--创建Channel(三)

    恩~,没错,其实这一篇才是真正的开始分析源码,你打我呀~. 先看一下我Netty的启动类 private void start() throws Exception { EventLoopGroup ...

  5. vuex分模块3

    nuxt 踩坑之 -- Vuex状态树的模块方式使用 原创 2017年12月20日 11:24:14 标签: vue / nuxt / vuex / 模块化 / 状态管理 874 初次看到这个模块方式 ...

  6. Mac sublime text3 安装插件

    一.下载Mac版sublime text3 下载地址:http://www.pc6.com/mac/120663.html(参考) 2.安装后打开 1.在界面的最上端找到tools(英文版),选择第一 ...

  7. vSphere Client克隆虚拟机

    免费的VMWare ESXi5.5非常强大,使用ESXi经常会遇到这样的问题,我需要建立多个虚拟机,系统一个一个安装很麻烦.VMware ESXi.VMware vCenter Server 和 vS ...

  8. Java多线程(2)线程锁

    多线程访问同一个资源进行读写操作,就很容易出一些问题(比如我们常见的读者写者,生产者消费者模型)所以我们会选择对他们设置信号量或者加锁,来限制同一个时刻只有一个线程对某个对象进行操作. 多线程是一个蛮 ...

  9. Codeforces 760B:Frodo and pillows(二分)

    http://codeforces.com/problemset/problem/760/B 题意:有n张床m个枕头,每张床可以有多个枕头,但是相邻的床的枕头数相差不能超过1,问第k张床最多能拥有的枕 ...

  10. 西门子PLC中各个组织块OB作用(OB1、OB100……)

    1.自由循环组织块OB1 S7  CPU启动完成后,操作系统循环执行OB1,OB1执行完成后,操作系统再次启动OB1.在OB1中可以调用FB.SFB.FC.SFC等用户程序使其循环执行.除OB90以外 ...