Spring Cloud Gateway实现接口防篡改和防重放
Spring Cloud Gateway 是基于 Spring Framework 5.0 和 Spring Boot 2.0 构建的 API 网关,提供路由等功能。其旨在提供一种简单而有效的方法路由到 API,并为它们提供跨领域的关注点,例如:安全性、监视 / 指标和弹性。
文章目录
前言
.在云计算、大数据等技术日趋成熟的情况下,微服务架构逐渐进入人们的视线,微服务架构的本质是把整体的业务拆分成特定明确功能的服务,在分布式环境下,随着微服务架构的广泛应用,各个服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务,这些服务之间的调用非常复杂,为了配合新核心建设,保障系统间系统传递的安全性,需要设计报文防篡改的技术。
一、防篡改是什么?
防篡改(英语:Tamper resistance)是指通过包装、系统或其他物理措施抗击产品正常用户的篡改(tamper,故意引发故障或造成破坏)的行为。
二、防重放是什么?
入侵者 C 可以从网络上截获 A 发给 B 的报文。C 并不需要破译这个报文(因为这
可能很花很多时间)而可以直接把这个由 A 加密的报文发送给 B,使 B 误认为 C 就是 A。然后
B 就向伪装是 A 的 C 发送许多本来应当发送给 A 的报文
三.防篡改和防重放的解决方式
- 可以通过时间戳,将时间戳放在header头中进行处理
- 通过时间戳 + sign签名处理,通过将报文参数进行相应的md5进行签名处理,同时将时间戳和sign放在header中,网关进行相应的验签证明请求的合法性
- 通过时间戳+随机数(norce)+sign签名的方式进行处理
流程如下:
- 用户发送请求,带上时间戳,随机数和sign数据
- 请求到网关,网关校验相关的数据
- 如果校验失败是直接返回,校验成功就跳转到后台的业务系统服务中
四.Spring Cloud Gateway实现
Spring Cloud Gateway是通过webflux实现的,我们现在进行代码coding
创建过滤器SignatureAntiReplayFilter
代码如下:
@Slf4j
@Component
public class SignatureAntiReplayFilter implements WebFilter, Ordered {
@Resource
private ReactiveCredentialsFilter reactiveCredentialsFilter;
//设置一个开关,进行控制是否开启报文签名校验
@Value("${gateway.openSign:#{true}}")
private Boolean openSign;
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
log.info("===========签名校验过滤器======");
/***
**
* 这里面还可以添加其他的业务逻辑在里面
*/
//如果不开启的话就直接放行
if (Boolean.FALSE.equals(openSign)) {
return webFilterChain.filter(serverWebExchange);
}
return SignatureAntiReplayMono.mono(serverWebExchange, webFilterChain);
}
@Override
public int getOrder() {
return -2;
}
}
创建SignatureAntiReplayMono处理类
代码如下:
@Slf4j
public class SignatureAntiReplayMono {
private SignatureAntiReplayMono() {
}
/**
* 处理过滤器
*
* @param serverWebExchange serverWebExchange
* @param webFilterChain 过滤器链
* @return 返回mono
*/
public static Mono<Void> mono(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
ServerHttpResponse serverHttpResponse = serverWebExchange.getResponse();
ServerHttpRequest serverHttpRequest = serverWebExchange.getRequest();
HttpHeaders httpHeaders = serverWebExchange.getRequest().getHeaders();
List<String> contentType = httpHeaders.get(HttpHeaders.CONTENT_TYPE);
if (!CollectionUtils.isEmpty(contentType) && !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
Result<Void> checkMap = AccessRequestCheck.checkParam(serverWebExchange.getRequest().getHeaders());
if (Objects.equals(String.valueOf(HttpStatus.OK.value()), checkMap.getCode())) {
return webFilterChain.filter(serverWebExchange);
} else {
try {
return serverHttpResponse.writeWith(Flux.just(serverHttpResponse.bufferFactory().wrap(ObjectMapperUtil.getObjectMapper().writeValueAsString(checkMap).getBytes(StandardCharsets.UTF_8))));
} catch (JsonProcessingException e) {
return Mono.empty();
}
}
}
ServerRequest serverRequest = ServerRequest.create(serverWebExchange, HandlerStrategies.withDefaults().messageReaders());
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(originalBody -> modifyBody()
.apply(serverWebExchange, originalBody));
HttpHeaders headers = new HttpHeaders();
headers.putAll(serverWebExchange.getRequest().getHeaders());
headers.remove("Content-Length");
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
CusCachedBodyOutputMessage outputMessage = new CusCachedBodyOutputMessage(serverWebExchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
ServerHttpRequest decorator = decorate(serverWebExchange, headers, outputMessage);
return webFilterChain.filter(serverWebExchange.mutate().request(decorator).build());
})).onErrorResume((throwable) -> {
return release(outputMessage, (Throwable) throwable);
});
}
protected static Mono<Void> release(CusCachedBodyOutputMessage outputMessage, Throwable throwable) {
return Boolean.TRUE.equals(outputMessage.isCached()) ? outputMessage.getBody().map(DataBufferUtils::release).then(Mono.error(throwable)) : Mono.error(throwable);
}
static ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CusCachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(headers);
if (contentLength > 0L) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set("Transfer-Encoding", "chunked");
}
return httpHeaders;
}
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
private static BiFunction<ServerWebExchange, String, Mono<String>> modifyBody() {
return (serverWebExchange, raw) -> {
try {
if (StringUtils.isBlank(raw)) {
raw = "{}";
}
Result<Void> checkMap = AccessRequestCheck.checkParam(serverWebExchange.getRequest().getHeaders(), raw);
if (Objects.equals(String.valueOf(HttpStatus.OK.value()), checkMap.getCode())) {
return Mono.just(raw);
} else {
return Mono.error();
}
} catch (Exception e) {
return Mono.error();
}
};
}
}
创建相关的AccessRequestCheck类
代码如下:
/**
* 请求过期时间
*/
public static final long EXPIRE_TIME = 60000;
/**
* 时间戳属性
*/
public static final String TIMESTAMP_PROPERTIES = "timestamp";
/**
* 签名属性
*/
public static final String SIGN_PROPERTIES = "sign";
/**
* 盐
*/
public static final String NORCE_PROPERTIES = "norce";
public static StringRedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public static Result<Void> checkParam(HttpHeaders headers, String body) {
Result<Void> result = new Result<>();
//获取header中的时间戳
List<String> timestamp = headers.get(TIMESTAMP_PROPERTIES);
//获取uuid 这里设置为盐
List<String> uuid = headers.get(NORCE_PROPERTIES);
//从header头中添加签名,签名使用请求参数进行加密生成sign
List<String> sign = headers.get(SIGN_PROPERTIES);
if (CollectionUtils.isEmpty(timestamp)) {
result.setCode(ErrorCodeEnum.TIMESTAMP_PARAM_MISSING.getCode());
result.setMsg(ErrorCodeEnum.TIMESTAMP_PARAM_MISSING.getMsg());
return result;
}
if (CollectionUtils.isEmpty(uuid)) {
result.setCode(ErrorCodeEnum.NORCE_PROPERTIES.getCode());
result.setMsg(ErrorCodeEnum.NORCE_PROPERTIES.getMsg());
return result;
}
if (CollectionUtils.isEmpty(sign)) {
result.setCode(ErrorCodeEnum.SIGN_PARAM_MISSING.getCode());
result.setMsg(ErrorCodeEnum.SIGN_PARAM_MISSING.getMsg());
return result;
}
//判断请求是否过期
if (!checkExpire(Long.parseLong(timestamp.get(0)))) {
result.setCode(ErrorCodeEnum.REQUEST_EXPIRE.getCode());
result.setMsg(ErrorCodeEnum.REQUEST_EXPIRE.getMsg());
return result;
}
//判断是否重复请求
if (Objects.nonNull(redisTemplate.opsForValue().get(uuid.get(0)))) {
result.setCode(ErrorCodeEnum.REQUEST_REPLACE.getCode());
result.setMsg(ErrorCodeEnum.REQUEST_REPLACE.getMsg());
return result;
}
//校验签名
if (!SignUtil.verifySign(body, sign.get(0), uuid.get(0))) {
result.setCode(ErrorCodeEnum.SIGN_ERROR.getCode());
result.setMsg(ErrorCodeEnum.SIGN_ERROR.getMsg());
return result;
}
redisTemplate.opsForValue().set(uuid.get(0), sign.get(0), EXPIRE_TIME, TimeUnit.MILLISECONDS);
result.setCode(String.valueOf(HttpStatus.OK.getCode()));
result.setMsg(HttpStatus.OK.getMessageEn());
return result;
}
/**
* 对时间戳和uuid进行校验
*
* @param headers 请求头
* @return
*/
public static Result<Void> checkParam(HttpHeaders headers) {
Result<Void> result = new Result<>();
//获取header中的时间戳
List<String> timestamp = headers.get(TIMESTAMP_PROPERTIES);
//获取uuid 这里设置为盐
List<String> uuid = headers.get(UUID_PROPERTIES);
if (CollectionUtils.isEmpty(timestamp)) {
result.setCode(ErrorCodeEnum.TIMESTAMP_PARAM_MISSING.getCode());
result.setMsg(ErrorCodeEnum.TIMESTAMP_PARAM_MISSING.getMsg());
return result;
}
if (CollectionUtils.isEmpty(uuid)) {
result.setCode(ErrorCodeEnum.NORCE_PROPERTIES.getCode());
result.setMsg(ErrorCodeEnum.NORCE_PROPERTIES.getMsg());
return result;
}
//判断请求是否过期
if (!checkExpire(Long.parseLong(timestamp.get(0)))) {
result.setCode(ErrorCodeEnum.REQUEST_EXPIRE.getCode());
result.setMsg(ErrorCodeEnum.REQUEST_EXPIRE.getMsg());
return result;
}
//判断是否重复请求
if (Objects.nonNull(redisTemplate.opsForValue().get(uuid.get(0)))) {
result.setCode(ErrorCodeEnum.REQUEST_REPLACE.getCode());
result.setMsg(ErrorCodeEnum.REQUEST_REPLACE.getMsg());
return result;
}
redisTemplate.opsForValue().set(uuid.get(0), uuid.get(0), EXPIRE_TIME, TimeUnit.MILLISECONDS);
result.setCode(String.valueOf(HttpStatus.OK.getCode()));
result.setMsg(HttpStatus.OK.getMessageEn());
return result;
}
/**
* 检测请求时间是否过期
*
* @param requestTime 请求时间
* @return 返回检测结果
*/
private static boolean checkExpire(long requestTime) {
return EXPIRE_TIME > System.currentTimeMillis() - requestTime;
}