SpringBoot接⼝如何设计防篡改、防重放攻击
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<>(IgnoreSignUri());
String requestUri = RequestURI();
boolean isMatch = false;
for (String uri : uriSet) {
isMatch = ains(uri);
if (isMatch) {
break;
}
}
log.info("当前请求的URI是==>{},isMatch==>{}", RequestURI(), isMatch);
if (isMatch) {
filterChain.doFilter(requestWrapper, response);
return;
}
String sign = Header("Sign");
Long timestamp = Header("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 > SignTimeout()) {
returnFail("已过期的签名", response);
log.info("前端时间戳:{},服务端时间戳:{}", DateUtil.date(timestamp * 1000), DateUtil.date());
return;
}
boolean accept = true;
SortedMap<String, String> paramMap;
switch (Method()) {
case "GET":
paramMap = UrlParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, sign, timestamp);
break;
case "POST":
case "PUT":
case "DELETE":
paramMap = BodyParams(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 = Writer();
String result = JSONString(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 + JSONString(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.Bytes()).toUpperCase();
}
}
不做签名验证的接⼝做成配置(l)
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);
}
springboot推荐算法@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请求⽰例
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论