1. Gateway overview
The so-called API gateway refers to the unified entrance of the system. It encapsulates the internal structure of the application and provides unified services for the client. Some public logic independent of the function of the business itself can be realized here, such as authentication, authentication, monitoring, routing and forwarding, etc.
Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux. It is not suitable for traditional servlet (spring MVC) containers.
keyword:
- Route: the basic building block of the gateway. It is defined by ID, target URI, predicate set and filter set. If the aggregation predicate is true, the route is matched.
- Predicate: matches any content from the HTTP request, such as URL, Header, parameter, etc.
- Filter: an instance built using a specific factory, through which requests and responses can be modified before or after sending downstream requests.
Workflow:
The work flow is as follows:
- User sends request to Gateway
- The request will first be extracted by the HttpWebHandlerAdapter and assembled into a gateway context
- The context of the gateway is then passed to the dispatcher handler, which is responsible for distributing the request to the RoutePredicateHandlerMapping
- Routepredictehandlermapping is responsible for finding routes and judging whether routes are available according to route assertions
- If the assertion is successful, the FilteringWebHandler creates a filter chain and calls
- The request will go through the method of PreFilter - microservice - PostFilter once, and finally return the response
2. Build basic gateway service
Use SpringCloudAlibaba+Nacos to realize the integration operation of Gateway. Nacos construction reference:
Docker deploying Nacos-2.0.3 stand-alone environment
Docker deploying Nacos-2.0.3 cluster environment
Deploying Nacos-2.0.3 stand-alone environment for Linux
Deploying Nacos-2.0.3 cluster environment for Linux
During nacos integration, you should pay attention to:
- If the gateway uses a namespace, it should be consistent with the namespaces of other services in the project.
- If the gateway uses group, it should be consistent with the group of other services in the project.
2.1 pom
<!--gateway gateway--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--nacos client--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
2.2 yaml configuration
server: port: 8085 spring: application: name: demo-gateway cloud: nacos: discovery: server-addr: 192.168.0.111:8848 # If you use a namespace, it should be consistent with the namespaces of other services in the project namespace: xxx # If group is used, it should be consistent with the group of other services in the project group: xxx gateway: discovery: locator: # Let gateway discover microservices in nacos enabled: true routes: # Routing array [routing is to specify which micro service to go to when the request meets what conditions] - id: user-service # The identifier of the current route. It is required to be unique (arbitrary) uri: lb://User service # LB refers to obtaining microservices by name from nacos and following load balancing policies order: 1 #The smaller the sorting, the higher the matching priority predicates: - Path=/user/** # When the request Path meets the rules specified by Path, route forwarding can be carried out, and / user is the servlet of the service Context Path name
Forwarding rules
When the request meets the path configured by predictions, the request path will be forwarded to the uri path, such as: http://127.0.0.1:8085/user/list
In the above request, if the request address: / user/list meets the matching rules, it will be forwarded. After forwarding, the request is: user service / user/list, that is, it will be forwarded to request user service, and the request path is / user/list
Simplified version
If the assertion matching rule of the configured route is consistent with the name of the service in the registry, you can access it directly through the gateway address / micro service / interface without configuring routes.
2.3 verification test:
Start the Gateway service and service business service, and realize the request forwarding by accessing the Gateway address of the interface;
In order to facilitate the verification of execution, some codes (pseudo codes) of the service business service are as follows:
# Service business service, Department configuration server: port: 8083 servlet: context-path: /user .... ....
// Service business service, some Controller codes @RestController public class UserController { @GetMapping("list") public void listUser() { System.out.println("Successful request user/list Interface"); } }
List of registry services:
Direct access to business services:
Request address: http//localhost:8083/user/list
Forward to business service through Gateway:
Request address: http//localhost:8085/user/list
If the business service can be successfully accessed through the address of the Gateway service, it indicates that the Gateway service is successfully built
3. Http timeout configuration
A unified Http timeout (response and connection) can be configured for all routes, or a timeout can be configured separately for a route (which will override the global configuration).
3.1 global timeout configuration
Connect timeout: connection timeout duration, in milliseconds. The default value is 45 seconds.
Response timeout: response timeout duration, in seconds. s is added to indicate seconds.
spring: cloud: gateway: httpclient: connect-timeout: 10000 response-timeout: 5s
3.2 routing timeout
Connect timeout: connection timeout duration, in milliseconds. The default value is the duration in global configuration.
Response timeout: response timeout duration, in milliseconds. The default value is the duration in global configuration.
spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/user/** # Set individual route timeout in metadata metadata: response-timeout: 200 connect-timeout: 200
4. Cross domain configuration
Note: if the gateway service is configured across domains, the business service should not be configured across domains. Otherwise, conflicts will occur and the gateway cross domain configuration will fail.
There are two ways of cross domain Configuration, by configuring yaml or using @ Configuration annotation
4.1 mode 1: yaml configuration
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOrigins: "*" allowedHeaders: "*" allowedMethods: "*"
4.2 mode 2: @ Configuration annotation
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.cors.reactive.CorsUtils; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; @Configuration public class GlobalCorsConfig { private static final String MAX_AGE = "18000L"; @Bean public WebFilter corsFilter() { return (ServerWebExchange ctx, WebFilterChain chain) -> { ServerHttpRequest request = ctx.getRequest(); if (CorsUtils.isCorsRequest(request)) { HttpHeaders requestHeaders = request.getHeaders(); ServerHttpResponse response = ctx.getResponse(); HttpHeaders headers = response.getHeaders(); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin()); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With"); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*"); headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE); if (request.getMethod() == HttpMethod.OPTIONS) { response.setStatusCode(HttpStatus.OK); return Mono.empty(); } } return chain.filter(ctx); }; } @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter() { return new HiddenHttpMethodFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange); } }; } }
5. Global filter
The global filter works on all routes without configuration. The global filter can realize the unified verification of permissions, log operation, security verification and other functions.
1. Built in filter
Spring cloud gateway also processes the whole route forwarding through a series of built-in global filters, as follows:
2. Custom global filter
The built-in filter can complete most functions, but we still need to write our own filter to realize custom operations, such as unified permission verification.
The code implementation is as follows:
@Configuration public class AuthGlobalFilter implements GlobalFilter, Ordered { //Complete judgment logic @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); //Get header HttpHeaders headers = request.getHeaders(); // Check whether the token is empty List<String> tokens = headers.get("token"); if (CollectionUtils.isEmpty(tokens)) { System.out.println("Authentication failed"); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); throw new RuntimeException("No permission"); } //Call chain The filter continues to execute downstream return chain.filter(exchange); } //Order. The smaller the value, the higher the priority @Override public int getOrder() { return 1; } }
6. Request filter and response filter
Data processing and printing of request data and response data are realized through request filter and response filter. The code is as follows:
Requestglobalfilter:
GatewayContext:
public class GatewayContext { /** * cache json body */ private String cacheBody; /** * cache formdata */ private MultiValueMap<String, String> formData; /** * cache reqeust path */ private String path; public String getCacheBody() { return cacheBody; } public void setCacheBody(String cacheBody) { this.cacheBody = cacheBody; } public MultiValueMap<String, String> getFormData() { return formData; } public void setFormData(MultiValueMap<String, String> formData) { this.formData = formData; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } }
RequestGlobalFilter:
@Component public class RequestGlobalFilter implements GlobalFilter, Ordered { private final Logger log = LoggerFactory.getLogger(RequestGlobalFilter.class); /** * default HttpMessageReader */ private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders(); private ServerHttpRequest request = null; private MediaType contentType = null; private HttpHeaders headers = null; private String path = null; private String ip = null; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Set start timestamp exchange.getAttributes().put("start", System.currentTimeMillis()); // Reset requested IP // Write the client ip, otherwise the ip obtained in the downstream service is the gateway ip ServerHttpRequest newRequest = exchange.getRequest().mutate().header("CLIENT_IP", ip).build(); exchange = exchange.mutate().request(newRequest).build(); // Initialize request and get path request = exchange.getRequest(); headers = request.getHeaders(); contentType = headers.getContentType(); path = request.getPath().pathWithinApplication().value(); /* * save request path and serviceId into gateway context */ GatewayContext gatewayContext = new GatewayContext(); gatewayContext.setPath(path); exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext); log.info("=>> Start: HttpMethod: {},Url: {}", request.getMethod(), request.getURI().getRawPath()); if (request.getMethod() == HttpMethod.GET) { // Record the parameter information of the request for the GET request MultiValueMap<String, String> queryParams = request.getQueryParams(); StringBuilder builder = new StringBuilder(); for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) { builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ",")).append(","); } log.info("MethodParam:{}", builder); } if (request.getMethod() == HttpMethod.POST || request.getMethod() == HttpMethod.PUT || request.getMethod() == HttpMethod.DELETE) { Mono<Void> voidMono = null; if (contentType != null) { if (StringUtils.contains(contentType.toString(), MediaType.APPLICATION_JSON.toString())) { voidMono = readBody(exchange, chain, gatewayContext); } if (StringUtils.contains(contentType.toString(), MediaType.APPLICATION_FORM_URLENCODED.toString())) { voidMono = readFormData(exchange, chain, gatewayContext); } if (StringUtils.contains(contentType.toString(), MediaType.MULTIPART_FORM_DATA_VALUE)) { voidMono = readFormData(exchange, chain, gatewayContext); } return voidMono; } } return chain.filter(exchange); } @Override public int getOrder() { return -3; } /** * ReadJsonBody * * @param exchange * @param chain * @return */ private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) { return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { /* * read the body Flux<DataBuffer>, 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() { return cachedFlux; } }; /** * 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 -> { //Request parameters log.info("MethodParam:{}", objectValue); gatewayContext.setCacheBody(objectValue); }).then(chain.filter(mutatedExchange)); }); } /** * ReadFormData * * @param exchange * @param chain * @return */ private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) { return exchange.getFormData() .doOnNext(multiValueMap -> { gatewayContext.setFormData(multiValueMap); //Pay attention to the handling of documents log.info("MethodParam:{}", multiValueMap); }).then(Mono.defer(() -> { Charset charset = contentType.getCharset(); charset = charset == null ? StandardCharsets.UTF_8 : charset; String charsetName = charset.name(); MultiValueMap<String, String> formData = gatewayContext.getFormData(); // formData is empty just return if (null == formData || formData.isEmpty()) { return chain.filter(exchange); } StringBuilder formDataBodyBuilder = new StringBuilder(); String entryKey; List<String> entryValue; try { // repackage form data for (Map.Entry<String, List<String>> entry : formData.entrySet()) { entryKey = entry.getKey(); entryValue = entry.getValue(); if (entryValue.size() > 1) { for (String value : entryValue) { formDataBodyBuilder.append(entryKey).append("=") .append( URLEncoder.encode(value, charsetName)) .append("&"); } } else { formDataBodyBuilder .append(entryKey).append("=").append(URLEncoder .encode(entryValue.get(0), charsetName)) .append("&"); } } } catch (UnsupportedEncodingException e) { // ignore URLEncode Exception } /** * substring with the last char '&' */ String formDataBodyString = ""; if (formDataBodyBuilder.length() > 0) { formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1); } /** * get data bytes */ byte[] bodyBytes = formDataBodyString.getBytes(charset); int contentLength = bodyBytes.length; ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator( request) { /** * change content-length * * @return */ @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); if (contentLength > 0) { httpHeaders.setContentLength(contentLength); } else { httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); } return httpHeaders; } /** * read bytes to Flux<Databuffer> * * @return */ @Override public Flux<DataBuffer> getBody() { return DataBufferUtils .read(new ByteArrayResource(bodyBytes), new NettyDataBufferFactory( ByteBufAllocator.DEFAULT), contentLength); } }; ServerWebExchange mutateExchange = exchange.mutate().request(decorator).build(); return chain.filter(mutateExchange); })); } }
Responseglobalfilter:
@Component public class ResponseGlobalFilter implements GlobalFilter, Ordered { private final Logger log = LoggerFactory.getLogger(ResponseGlobalFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Get response object ServerHttpResponse response = exchange.getResponse(); DataBufferFactory bufferFactory = response.bufferFactory(); ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { //Record response log long end = System.currentTimeMillis(); long start = Long.parseLong(exchange.getAttribute("start").toString()); long useTime = end - start; if (body instanceof Flux) { // Get response ContentType String responseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); // Response body for recording JSON format data if (!StringUtils.isEmpty(responseContentType) && responseContentType.contains(MediaType.APPLICATION_JSON_VALUE)) { Flux<? extends DataBuffer> fluxBody = Flux.from(body); // Solve the problem of segmented transmission of return volume return super.writeWith(fluxBody.buffer().map(dataBuffers -> { DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); DataBuffer join = dataBufferFactory.join(dataBuffers); byte[] content = new byte[join.readableByteCount()]; join.read(content); DataBufferUtils.release(join); String responseData = new String(content, StandardCharsets.UTF_8); String responseStr = responseData.replaceAll("\n", "").replaceAll("\t", ""); responseStr = responseStr.length() > 500 ? responseStr.substring(500) : responseStr; log.info("=>> END: Time: {}ms", useTime); log.info("RESPONSE INFO = {}", responseStr); return bufferFactory.wrap(responseData.getBytes()); })); } } return super.writeWith(body); } }; return chain.filter(exchange.mutate().response(responseDecorator).build()); } @Override public int getOrder() { return -2; } }
effect:
7. Global exception handling mechanism
7.1 unified response
public class ResponseObject { /** * information */ private String msg; /** * Response code */ private int code; /** * data */ private Object data; public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } // Return data public static ResponseObject success(Object object) { ResponseObject responseObject = new ResponseObject(); responseObject.setData(object); responseObject.setCode(200); responseObject.setMsg("Operation successful"); return responseObject; } // Return information public static ResponseObject success(String msg) { ResponseObject responseObject = new ResponseObject(); responseObject.setData(true); responseObject.setCode(200); responseObject.setMsg(msg); return responseObject; } // Direct return public static ResponseObject success() { ResponseObject responseObject = new ResponseObject(); responseObject.setData(true); responseObject.setCode(200); responseObject.setMsg("Operation successful"); return responseObject; } // Custom return data and information public static ResponseObject success(Object object, String msg) { ResponseObject responseObject = new ResponseObject(); responseObject.setData(object); responseObject.setCode(200); responseObject.setMsg(msg); return responseObject; } // Custom return data and content public static ResponseObject fail(Object data, String msg) { ResponseObject responseObject = new ResponseObject(); responseObject.setData(data); responseObject.setCode(500); responseObject.setMsg(msg); return responseObject; } // Custom return information public static ResponseObject fail(String msg) { ResponseObject responseObject = new ResponseObject(); responseObject.setData(false); responseObject.setCode(400); responseObject.setMsg(msg); return responseObject; } // Direct return public static ResponseObject fail() { ResponseObject responseObject = new ResponseObject(); responseObject.setData(false); responseObject.setCode(400); responseObject.setMsg("operation failed"); return responseObject; } // Custom return information and encoding public static ResponseObject fail(String msg, int code) { ResponseObject responseObject = new ResponseObject(); responseObject.setData(false); responseObject.setCode(code); responseObject.setMsg(msg); return responseObject; } }
7.2 create ErrorHandlerConfiguration
@Configuration @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class}) public class ErrorHandlerConfiguration { private final ServerProperties serverProperties; private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public ErrorHandlerConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties, ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { this.serverProperties = serverProperties; this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { JsonExceptionHandler exceptionHandler = new JsonExceptionHandler( errorAttributes, this.resourceProperties, this.serverProperties.getError(), this.applicationContext); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; } }
7.3 create JsonExceptionHandler
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler { private final static Logger log = LoggerFactory.getLogger(JsonExceptionHandler.class); public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } /** * Get exception properties */ @Override protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { Throwable error = super.getError(request); return this.buildMessage(request, error); } /** * Specifies that the response processing method is JSON processing method * * @param errorAttributes */ @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } /** * Get the corresponding HttpStatus according to the code * * @param errorAttributes */ @Override protected int getHttpStatus(Map<String, Object> errorAttributes) { return HttpStatus.OK.value(); } /** * Build exception response information * * @param request * @param throwable * @return */ private Map<String, Object> buildMessage(ServerRequest request, Throwable throwable) { Map<String, Object> map = new HashMap<>(8); log.error("[Gateway exception information]Request path:{},Exception information:{},Exception type:{}", request.path(), throwable.getMessage(), ExceptionUtils.getStackTrace(throwable)); //Encapsulate error messages map.put("message", throwable.getMessage()); map.put("data", false); return map; } }
7.4 modify ResponseGlobalFilter
In ResponseGlobalFilter, add the following code:
return super.writeWith(fluxBody.buffer().map(dataBuffers -> { .... .... //Convert the json returned by the service to the standard format ResponseObject object = JSON.parseObject(responseData, ResponseObject.class); //Exception thrown when resolving to object other than 200 if (object.getData() != null && object.getCode() != 200 && !(boolean) object.getData()) { throw new RuntimeException(object.getMsg()); } .... .... }
7.4 testing
In the service, throw an exception to test the handling in the Gateway
@RestController public class UserController { @GetMapping("list") public ResponseObject listUser() { // The global exception interceptor can also be used to simulate exceptions try { int a = 1 / 0; } catch (Exception e) { return ResponseObject.fail(); } return null; } }