background
There is a business requirement to provide a set of apis for third parties to call.
Before dealing with the specific business interface, a simple authentication should be done in the design. After the identity transmission parameters are negotiated, the identity verification is done in the Gateway module, considering that the Spring Cloud Gateway has been used in the project.
Therefore, when the server obtains a request, it must intercept and obtain the request transmission parameters before performing subsequent authentication logic.
Here’s a problem to solve: How does the Spring Cloud Gateway read the request pass parameter?
Spring Cloud Gateway get Request Body
Problem description
Problem: Spring Cloud Gateway reads request parameters
Only two cases are dealt with briefly here, GET and POST requests.
If a get request is found, the parameter on the URL is taken; If a POST request is found, the contents of the body are read.
The solution
Reference github.com/spring-clou…
Two filters are defined. The first filter, ApiRequestFilter, takes the parameters and places them in the GatewayContext.
Note If it is a POST request, after the request body is read, it must be reconstructed and filled back into the request body.
The second filter, ApiVerifyFilter, gets the parameters directly from the context.
Later, if other services also need to read parameters, they can directly obtain the parameters from the context, without repeating the logic of obtaining parameters.
The implementation code
GatewayContext
@Data
public class GatewayContext {
public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext";
/** * cache json body */
private String cacheBody;
/** * cache form data */
private MultiValueMap<String, Part> formData;
/** * cache request path */
private String path;
}
Copy the code
ApiRequestFilter
@Component
@Slf4j
public class ApiRequestFilter implements GlobalFilter.Ordered {
private static AntPathMatcher antPathMatcher;
static {
antPathMatcher = new AntPathMatcher();
}
/** * default HttpMessageReader */
private static finalList<HttpMessageReader<? >> messageReaders = HandlerStrategies.withDefaults().messageReaders();private static final ResolvableType MULTIPART_DATA_TYPE = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
private static final Mono<MultiValueMap<String, Part>> EMPTY_MULTIPART_DATA = Mono.just(CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<String, Part>(0))).cache();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
if(request.getMethod() == HttpMethod.GET){
// get request processing parameters
return handleGetMethod(exchange, chain, request);
}
if(request.getMethod() == HttpMethod.POST){
// Post request processing parameters
return handlePostMethod(exchange, chain, request);
}
return chain.filter(exchange);
}
/** * get request processing parameters *@param exchange
* @param chain
* @param request
* @return* /
private Mono<Void> handleGetMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
// TODO does not do processing temporarily
return chain.filter(exchange);
}
/** * Post request verification parameter *@param exchange
* @param chain
* @param request
* @return* /
private Mono<Void> handlePostMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request){
GatewayContext gatewayContext = new GatewayContext();
gatewayContext.setPath(request.getPath().pathWithinApplication().value());
/** * save gateway context into exchange */
exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext);
MediaType contentType = request.getHeaders().getContentType();
if(MediaType.APPLICATION_JSON.equals(contentType)
|| MediaType.APPLICATION_JSON_UTF8.equals(contentType)){
// Request content is application json
// Rebuild the request body
return readJsonBody(exchange, chain, gatewayContext);
}
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
// Request form data
return readFormData(exchange, chain, gatewayContext);
}
return chain.filter(exchange);
}
/** * POST request * restructures the request body *@param exchange
* @param chain
* @param gatewayContext
* @return* /
private Mono<Void> readJsonBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
/* * read the body Flux
, and release the buffer * //TODO when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature * see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095 */
byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); Flux<DataBuffer> cachedFlux = Flux.defer(() -> { DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); DataBufferUtils.retain(buffer);return Mono.just(buffer);
});
/** * repackage ServerHttpRequest */
ServerHttpRequest mutatedRequest =
new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody(a) {
returncachedFlux; }};/** * mutate exchage with new ServerHttpRequest */
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
/** * read body string with default messageReaders */
return ServerRequest.create(mutatedExchange, messageReaders)
.bodyToMono(String.class)
.doOnNext(objectValue -> {
// save body into gatewayContext
gatewayContext.setCacheBody(objectValue);
})
.then(chain.filter(mutatedExchange));
});
}
private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) {
return exchange.getRequest().getBody().collectList().flatMap(dataBuffers -> {
final byte[] totalBytes = dataBuffers.stream().map(dataBuffer -> {
try {
final byte[] bytes = IOUtils.toByteArray(dataBuffer.asInputStream());
// System.out.println(new String(bytes));
return bytes;
} catch (IOException e) {
throw new RuntimeException(e);
}
}).reduce(this::addBytes).get();
final ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody(a) {
returnFlux.just(buffer(totalBytes)); }};final ServerCodecConfigurer configurer = ServerCodecConfigurer.create();
final Mono<MultiValueMap<String, Part>> multiValueMapMono = repackageMultipartData(decorator, configurer);
return multiValueMapMono.flatMap(part -> {
for (String key : part.keySet()) {
// Enter the next loop if it is a file
if (key.equals("file")) {
continue;
}
part.getFirst(key).content().subscribe(buffer -> {
final byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
try {
final String bodyString = new String(bytes, "utf-8");
gatewayContext.setCacheBody(bodyString);
} catch(UnsupportedEncodingException e) { e.printStackTrace(); }}); }return chain.filter(exchange.mutate().request(decorator).build());
});
});
}
@SuppressWarnings("unchecked")
private static Mono<MultiValueMap<String, Part>> repackageMultipartData(ServerHttpRequest request, ServerCodecConfigurer configurer) {
try {
final MediaType contentType = request.getHeaders().getContentType();
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
return ((HttpMessageReader<MultiValueMap<String, Part>>) configurer.getReaders().stream().filter(reader -> reader.canRead(MULTIPART_DATA_TYPE, MediaType.MULTIPART_FORM_DATA))
.findFirst().orElseThrow(() -> new IllegalStateException("No multipart HttpMessageReader."))).readMono(MULTIPART_DATA_TYPE, request, Collections.emptyMap()) .switchIfEmpty(EMPTY_MULTIPART_DATA).cache(); }}catch (InvalidMediaTypeException ex) {
// Ignore
}
return EMPTY_MULTIPART_DATA;
}
/**
* addBytes.
* @param first first
* @param second second
* @return byte
*/
public byte[] addBytes(byte[] first, byte[] second) {
final byte[] result = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, result, first.length, second.length);
return result;
}
private DataBuffer buffer(byte[] bytes) {
final NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
final DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
@Override
public int getOrder(a) {
return FilterOrderConstant.getOrder(this.getClass().getName()); }}Copy the code
ApiVerifyFilter
@Component
@Slf4j
public class ApiVerifyFilter implements GlobalFilter.Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
if(request.getMethod() == HttpMethod.GET){
// get request verification parameters
return verifyGetMethod(exchange, chain, request);
}
if(request.getMethod() == HttpMethod.POST){
// Post requests validation parameters
return verifyPostMethod(exchange, chain, request);
}
return chain.filter(exchange);
}
/** * get Request validation parameter *@param exchange
* @param chain
* @param request
* @return* /
private Mono<Void> verifyGetMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
// get requests to obtain parameters
Map<String, String> queryParamMap = request.getQueryParams().toSingleValueMap();
// Specific service parameters
String secretId = queryParamMap.get("secretId");
String secretKey = queryParamMap.get("secretKey");
// Verify parameter logic
return verifyParams(exchange, chain, secretId, secretKey);
}
/** * Post request verification parameter *@param exchange
* @param chain
* @param request
* @return* /
private Mono<Void> verifyPostMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
try {
GatewayContext gatewayContext = (GatewayContext)exchange.getAttributes().get(GatewayContext.CACHE_GATEWAY_CONTEXT);
// get body from gatewayContext
String cacheBody = gatewayContext.getCacheBody();
Map map = new ObjectMapper().readValue(cacheBody, Map.class);
// Specific service parameters
String secretId = String.valueOf(map.get("secretId"));
String secretKey = String.valueOf(map.get("secretKey"));
// Verify parameter logic
return verifyParams(exchange, chain, secretId, secretKey);
} catch (Exception e){
log.error("Failed to parse body content: {}", e);
/ / 403
returnresponse(exchange, R.fail().enumCode(HttpCode.FORBIDDEN)); }}/** * Check parameter *@param exchange
* @param chain
* @param secretId
* @param secretKey
* @return* /
private Mono<Void> verifyParams(ServerWebExchange exchange, GatewayFilterChain chain, String secretId, String secretKey) {
// If the verification fails, a message is displayed
// return response(exchange, R.fail().enumCode(HttpCode.UNAUTHORIZED));
// todo
// If the verification succeeds, the current filter is complete
return chain.filter(exchange);
}
/** * response returns code *@param exchange
* @param r
* @return* /
private Mono<Void> response(ServerWebExchange exchange, R r) {
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
try {
byte[] bytes = new ObjectMapper().writeValueAsBytes(r);
DataBuffer buffer = originalResponse.bufferFactory().wrap(bytes);
return originalResponse.writeWith(Flux.just(buffer));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null; }}@Override
public int getOrder(a) {
return FilterOrderConstant.getOrder(this.getClass().getName()); }}Copy the code