一 防篡改是什么?
防篡改(英语:Tamper resistance)是指通过包装、系统或其他物理措施抗击产品正常用户的篡改(tamper,故意引发故障或造成破坏)的行为。
二 防重放是什么?
入侵者 C 可以从网络上截获 A 发给 B 的报文。C 并不需要破译这个报文(因为这
可能很花很多时间)而可以直接把这个由 A 加密的报文发送给 B,使 B 误认为 C 就是 A。然后
B 就向伪装是 A 的 C 发送许多本来应当发送给 A 的报文
三 防篡改和防重放的解决方式
可以通过时间戳,将时间戳放在header头中进行处理
通过时间戳 + sign签名处理,通过将报文参数进行相应的md5进行签名处理,同时将时间戳和sign放在header中,网关进行相应的验签证明请求的合法性
通过时间戳+随机数(norce)+sign签名的方式进行处理
流程如下:
四 代码实现
1.Filter实现
/**
* 安全基线拦截器:
* 防重放、防篡改
*/
@Component
public class CosSecurityFilter extends ZuulFilter {
private final Logger log = LoggerFactory.getLogger(CosSecurityFilter.class);
@Resource
private CosSecurityProperties cosSecurityProperties;
@Resource
private CosSecurityUtil cosSecurityUtil;
@Resource
private AntPathMatcher antPathMatcher;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* 拦截顺序,越小越优先
*
* @return
*/
@Override
public int filterOrder() {
return -9;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
if (BooleanUtil.isTrue(cosSecurityProperties.getEnable())&& !ctx.getBoolean("transmit")) {
return true;
} else {
log.debug("【安全基线】未开启");
return false;
}
}
/**
* 如果安全校验不通过,请求上下文中会有isSecurityPass;
* isSecurityPass为true,代表安全校验通过
* isSecurityPass为false,代表安全校验不通过
*/
@Override
public Object run() {
log.info("---CosSecurityFilter---");
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String uri = request.getRequestURI();
String sign = request.getHeader(CosSecurityConstants.HEADER_SIGN);
String timestamp = request.getHeader(CosSecurityConstants.HEADER_TIMESTAMP);
String nonce = request.getHeader(CosSecurityConstants.HEADER_NONCE);
String clientVersion = request.getHeader(CosSecurityConstants.HEADER_VERSION);
String method = request.getMethod();
String contentType = request.getContentType();
try {
//url白名单
List<String> ignoreUrlList = cosSecurityProperties.getIgnoreUrlList();
if (CollectionUtil.isNotEmpty(ignoreUrlList)) {
for (String ignoreUrl : ignoreUrlList) {
if (antPathMatcher.match(uri, ignoreUrl)) {
return null;
}
}
}
switch (method) {
case ServletUtil.METHOD_POST:
//application/json才校验签名
if (contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
// 版本号不符合条件
if (StrUtil.isNotEmpty(clientVersion) && (!Pattern.matches(CosSecurityConstants.PATTERN_VERSION, clientVersion)
|| AppUtil.compareVersion(clientVersion, CosSecurityConstants.CONFIG_BASE_VERSION_VALUE) < 0)) {
if (cosSecurityProperties.getIsValidVersion()){
//如果开启版本号校验并且版本号为空或者不符合条件
throw new BusinessException(ExceptionEnum.VERSION_LOW.getCode(), ExceptionEnum.VERSION_LOW.getErrMsg());
} else {
//如果不开启版本号校验,并且版本在11以下 不验证签名
return null;
}
}
BodyReaderHttpServletRequestWrapper httpServletRequestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//校验签名
cosSecurityUtil.validPostSign(sign, Long.valueOf(timestamp), nonce, httpServletRequestWrapper);
ctx.setRequest(httpServletRequestWrapper);
//校验超时时间
cosSecurityUtil.validTimestamp(timestamp);
//校验随机数
cosSecurityUtil.validAndSaveNonce(nonce, request);
}
break;
case ServletUtil.METHOD_GET:
// GET请求不作处理
break;
default:
break;
}
ctx.set(CosSecurityConstants.KEY_IS_SECURITY_PASS, true);
} catch (BusinessException e) {
log.error("【安全请求校验】校验不通过,uri=[{}],timestamp=[{}],nonce=[{}],sign=[{}],errorMessage=[{}]", uri, timestamp, nonce, sign, e.getMessage());
log.error("error:",e);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
ResultMessage result = new ResultMessage(false, e.getCode(), e.getErrMsg());
ctx.getResponse().setCharacterEncoding("UTF-8");
ctx.getResponse().setContentType("application/json; charset=utf-8");
ctx.setResponseBody(JSON.toJSONString(result, SerializerFeature.BrowserCompatible));
ctx.set(CosSecurityConstants.KEY_IS_SECURITY_PASS, false);
} catch (Exception e) {
log.error("【安全请求校验】校验失败,uri=[{}],timestamp=[{}],nonce=[{}],sign=[{}],errorMessage=[{}]", uri, timestamp, nonce, sign, e.getMessage());
log.error("error:",e);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
ResultMessage result = new ResultMessage(false, ExceptionEnum.SIGN_INVALID.getCode(), ExceptionEnum.SIGN_INVALID.getErrMsg());
ctx.getResponse().setCharacterEncoding("UTF-8");
ctx.getResponse().setContentType("application/json; charset=utf-8");
ctx.setResponseBody(JSON.toJSONString(result, SerializerFeature.BrowserCompatible));
ctx.set(CosSecurityConstants.KEY_IS_SECURITY_PASS, false);
}
return null;
}
}
2.工具类实现
@Component
public class CosSecurityUtil {
private final Logger log = LoggerFactory.getLogger(CosSecurityUtil.class);
@Resource
private CosSecurityProperties cosSecurityProperties;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 校验请求有效时间
*
* @param time
* @return
*/
public void validTimestamp(String time) throws Exception {
if (StrUtil.isEmpty(time)) {
throw new BusinessException(ExceptionEnum.TIMESTAMP_INVALID.getCode(), ExceptionEnum.TIMESTAMP_INVALID.getErrMsg());
}
if (!Pattern.matches(CosSecurityConstants.PATTERN_TIMESTAMP, time)) {
throw new BusinessException(ExceptionEnum.TIMESTAMP_INVALID.getCode(), ExceptionEnum.TIMESTAMP_INVALID.getErrMsg());
}
Long timestamp = Long.valueOf(time);
//服务器UTC时间
LocalDateTime utcLocalDateTime = Instant.now().atZone(ZoneId.of("UTC")).toLocalDateTime();
LocalDateTime severalMinutesBefore = LocalDateTimeUtil.offset(utcLocalDateTime, -10L, ChronoUnit.MINUTES);
LocalDateTime severalMinutesAfter = LocalDateTimeUtil.offset(utcLocalDateTime, 10L, ChronoUnit.MINUTES);
if (timestamp > LocalDateTimeUtil.toEpochMilli(severalMinutesBefore) && timestamp < LocalDateTimeUtil.toEpochMilli(severalMinutesAfter)) {
//do nothing
} else {
//签名超时
throw new BusinessException(ExceptionEnum.SIGN_TIMEOUT.getCode(), ExceptionEnum.SIGN_TIMEOUT.getErrMsg());
}
}
/**
* 校验nonce,并保存至redis中
*
* @param nonce
* @param request
* @return
*/
public void validAndSaveNonce(String nonce, HttpServletRequest request) throws Exception {
if (StrUtil.isEmpty(nonce)) {
throw new BusinessException(ExceptionEnum.NONCE_INVALID.getCode(), ExceptionEnum.NONCE_INVALID.getErrMsg());
}
if (!Pattern.matches(CosSecurityConstants.PATTERN_NONCE, nonce)) {
throw new BusinessException(ExceptionEnum.NONCE_INVALID.getCode(), ExceptionEnum.NONCE_INVALID.getErrMsg());
}
String remoteHost = request.getRemoteHost();
String method = request.getMethod();
String requestUrl = request.getRequestURL().toString();
JSONObject nonceRedisValue = new JSONObject();
nonceRedisValue.put("remoteHost", remoteHost);
nonceRedisValue.put("method", method);
nonceRedisValue.put("requestUrl", requestUrl);
String nonceRedisKey = CosSecurityConstants.PREFIX_NONCE_KEY + nonce;
Boolean hasKey = stringRedisTemplate.hasKey(nonceRedisKey);
if (null != hasKey && hasKey) {
throw new BusinessException(ExceptionEnum.REQUEST_REPEAT.getCode(), ExceptionEnum.REQUEST_REPEAT.getErrMsg());
}
RBucket<String> nonceBucket = redissonClient.getBucket(nonceRedisKey);
boolean isSuccess = nonceBucket.trySet(nonceRedisValue.toJSONString(), 10L, TimeUnit.MINUTES);
if (!isSuccess) {
throw new BusinessException(ExceptionEnum.REQUEST_REPEAT.getCode(), ExceptionEnum.REQUEST_REPEAT.getErrMsg());
}
}
/**
* 校验POST请求的sign
*
* @param sign
* @param timestamp
* @param nonce
* @throws Exception
*/
public void validPostSign(String sign, Long timestamp, String nonce, BodyReaderHttpServletRequestWrapper request) throws Exception {
if (StrUtil.isEmpty(sign)) {
throw new BusinessException(ExceptionEnum.SIGN_INVALID.getCode(), ExceptionEnum.SIGN_INVALID.getErrMsg());
}
if (!Pattern.matches(CosSecurityConstants.PATTERN_SIGN, sign)) {
throw new BusinessException(ExceptionEnum.SIGN_INVALID.getCode(), ExceptionEnum.SIGN_INVALID.getErrMsg());
}
//取请求body体的md5摘要
String reqBody = request.getBodyString();
log.info("【安全基线-POST请求签名校验】requestBody=[{}]", reqBody);
String body = DigestUtils.md5Hex(reqBody);
//计算规则:sign=md5(timestamp+nonce+body+key)
String signString = "timestamp=" + timestamp + "&nonce=" + nonce + "&body=" + body +"&key=" + cosSecurityProperties.getSecurityCode();
log.info("验签字符串拼接结果为:{}",signString);
String serverSign = DigestUtils.md5Hex(signString);
log.info("【安全基线-POST请求签名校验】前端sign=[{}],后端计算body=[{}],后端sign=[{}]", sign, body, serverSign);
if (!StrUtil.equals(sign.toLowerCase(), serverSign.toLowerCase())) {
throw new BusinessException(ExceptionEnum.SIGN_INVALID.getCode(), ExceptionEnum.SIGN_INVALID.getErrMsg());
}
}
public static void main(String[] args){
String reqBody = "";
String body = DigestUtils.md5Hex(reqBody);
LocalDateTime utcLocalDateTime = Instant.now().atZone(ZoneId.of("UTC")).toLocalDateTime();
Long timestamp = LocalDateTimeUtil.toEpochMilli(utcLocalDateTime);
System.out.println(timestamp);
String nonce = "eb8f198d36b3edcb913ade2506707631";
//计算规则:sign=md5(timestamp+nonce+body+key)
String signString = "timestamp=" + timestamp + "&nonce=" + nonce + "&body=" + body +"&key=" + "RETAILCLOUD@HUA123";
String serverSign = DigestUtils.md5Hex(signString);
System.out.println(serverSign);
}
}