After the Netflix open source abortion, Spring is constantly replacing Netflix related components, such as Eureka, Zuul, Feign, Ribbon, etc. Zuul's alternative product is SpringCloud Gateway, which is a gateway component developed by the Spring team, which can realize new features such as security authentication, current limiting, Retry, and support for long connections.
Background description
If there are three services, i.e. product service and account service. Now there are client WEB applications or APP applications that need to access back-end services to obtain data, so the access paths of the three services need to be maintained on the client.
Such an architecture will have the following typical problems:
- Each microservice needs to open external network access rights and configure a separate access domain name. For each new service, the operation and maintenance personnel need to configure the domain name mapping first
- The client needs to maintain the access addresses of all microservices. Imagine if there are hundreds of microservices
- When the service needs to control the permission of the interface, and the user must be authenticated before calling, then all the permission logic must be rewritten on the server side
- . . .
Therefore, a gateway service needs to be added before the micro service, so that all clients can access the gateway and the gateway is responsible for forwarding requests; Put the permission verification logic into the filter of the gateway, and the back-end service no longer needs to pay attention to the code of permission verification; You only need to provide a domain name address that can be accessed by the external network. After the new service is added, you don't need to ask the operation and maintenance personnel to configure the network. In this way, the above architecture is as follows:
GateWay introduction
In the spring cloud architecture, a separate gateway service needs to be deployed to provide external access, and then the gateway service forwards the request to the specific back-end service according to the configured rules.
Spring Cloud Gateway is a new sub project of spring cloud, which is based on spring 5 x,SpringBoot2.x technology version is written to provide a simple, convenient and scalable unified API routing management method.
Basic concepts
-
Route
Routing is the basic unit of gateway, which is composed of ID, URI, a group of Predicate and a group of Filter. It is matched and forwarded according to the Predicate
-
Predicate (assertion)
It refers to the Function Predicate of Java 8, and the input type is ServerWebExchange in the Spring framework. As the judgment condition of routing and forwarding, Spring cloud gateway currently supports a variety of methods, such as Path, Query, Method, Header, etc
-
Filter
The filter is the filtering logic that passes through when routing and forwarding requests. The instance of GatewayFilter can be used to modify the contents of requests and responses
Workflow
The client sends a request to the Spring Cloud Gateway. If the gateway handler mapping determines that the request matches the route, it is sent to the gateway Web handler. This handler sends requests through a request specific filtering chain at run time. The reason why filters are separated by dashed lines is that filters can execute logic before or after sending proxy requests.
Predicates assertion
When this condition is met, it will be forwarded. If there are multiple, it will be forwarded when all conditions are met.
Matching mode | explain | Sample |
---|---|---|
Before | Before a certain point in time | Before=2019-05-01T00:00:00+08:00[Asia/Shanghai] |
After | After a certain point in time | After=2019-04-29T00:00:00+08:00[Asia/Shanghai] |
Between | Before +After | Between=2019-04-29T00:00:00+08:00[Asia/Shanghai], 2019-05-01T00:00:00+08:00[Asia/Shanghai] |
Cookie | Cookie value | Cookie=hacfin, langyastudio |
Header | Header value | Header=X-Request-Id, \d+ |
Host | host name | Host=**.langyastudio.com |
Method | Request mode | Method=POST |
Query | Request parameters | Query=xxx, zzz |
Path | Request path | Path=/article/{articleId} |
RemoteAddr | Request IP | RemoteAddr=192.168.1.56/24 |
Weight | weight | Weight=group1, 8 |
Weight example:
80% of the requests will be routed to localhost:8201 and 20% to localhost:8202
spring: cloud: gateway: routes: - id: weight_high uri: http://localhost:8201 predicates: - Weight=group1, 8 - id: weight_low uri: http://localhost:8202 predicates: - Weight=group1, 2
Filter filter
Routing filters can be used to modify incoming HTTP requests and returned HTTP responses. Spring Cloud Gateway has built-in multiple routing filters, which are generated by the factory class of GatewayFilter. The usage of common routing filters is described below.
AddRequestParameter add parameter
Filter to add parameters to the request
spring: cloud: gateway: routes: - id: add_request_parameter_route uri: http://localhost:8201 filters: - AddRequestParameter=username, langyastudio predicates: - Method=GET
The above configuration will add the request parameter username=langyastudio to the GET request, and use the following command to test through curl tool
curl http://localhost:9201/user/getByUsername
Equivalent to initiating the request:
curl http://localhost:8201/user/getByUsername?username=langyastudio
StripPrefix prefix removal
A filter that removes a specified number of path prefixes
spring: cloud: gateway: routes: - id: strip_prefix_route uri: http://localhost:8201 predicates: - Path=/user-service/** filters: - StripPrefix=2
The above configuration will remove two bits from the path of the request starting with / user service /, and use the following command to test through curl tool
curl http://localhost:9201/user-service/a/user/1
Equivalent to initiating the request:
curl http://localhost:8201/user/1
PrefixPath prefix increase
In contrast to the StripPrefix filter, the filter that adds operations to the original path prefix
spring: cloud: gateway: routes: - id: prefix_path_route uri: http://localhost:8201 predicates: - Method=GET filters: - PrefixPath=/user
The above configuration will add / user path prefix to all GET requests, and use the following command to test through curl tool
curl http://localhost:9201/1
Equivalent to initiating the request:
curl http://localhost:8201/user/1
RequestRateLimiter current limiting
The RequestRateLimiter filter can be used to limit the flow. The RateLimiter implementation is used to determine whether to allow the current request to continue. If the request is too large, it will return HTTP 429 - too many request status by default.
In POM Add related dependencies to XML
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
Add the configuration class of current limiting policy. There are two kinds of policies. One is to limit the current according to the username in the request parameter, and the other is to limit the current according to the access IP
@Configuration public class RedisRateLimiterConfig { @Bean KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("username")); } @Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } }
Redis is used to limit the current, so the configuration of redis and RequestRateLimiter needs to be added. Here, all GET requests are limited by IP
server: port: 9201 spring: redis: host: localhost password: 123456 port: 6379 cloud: gateway: routes: - id: requestratelimiter_route uri: http://localhost:8201 filters: - name: RequestRateLimiter args: #Number of requests allowed to be processed per second redis-rate-limiter.replenishRate: 1 #The capacity of the token bucket, the maximum number of requests allowed to be completed in one second redis-rate-limiter.burstCapacity: 2 #Current limiting policy, Bean corresponding to the policy #SpEL Expression basis#{@ beanName} get Bean object from Spring container key-resolver: "#{@ipKeyResolver}" predicates: - Method=GET logging: level: org.springframework.cloud.gateway: debug
Multiple requests for this address: http://localhost:9201/user/1 , an error with status code 429 will be returned
Retry retry
The filter that retries the routing request can determine whether to retry according to the HTTP status code returned by the routing request
To modify a profile:
spring: cloud: gateway: routes: - id: retry_route uri: http://localhost:8201 predicates: - Method=GET filters: - name: Retry args: retries: 1 #Number of retries required statuses: BAD_GATEWAY #Which status code needs to be returned for retry? The status code returned is 5XX for retry backoff: firstBackoff: 10ms maxBackoff: 50ms factor: 2 basedOnPreviousValue: false
When the call returns 500, it will retry and access the test address: http://localhost:9201/user/111
It can be found that the user service console reported an error twice, indicating a retry
2019-10-27 14:08:53.435 ERROR 2280 --- [nio-8201-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause
Custom filter
For example, after using the spring cloud architecture, we hope that all requests can be accessed only through the gateway. Without any processing, we can bypass the gateway and directly access the back-end services.
There are three main solutions to prevent bypassing the gateway and directly requesting back-end services:
-
Network isolation
Common back-end services are deployed in the intranet, and only gateway applications are allowed to access back-end services through firewall policies
-
Application layer interception
When requesting back-end services, check whether the request comes from the gateway through the interceptor. If it does not come from the gateway, it will prompt that access is not allowed
-
Deploy using Kubernetes
When deploying the spring cloud architecture using Kubernetes, configure NodePort for the Service of the gateway, and use ClusterIp for the Service of other back-end services, so that only the gateway can be accessed outside the cluster
If application layer interception is adopted, add additional Header examples when the request passes through the gateway:
@Component @Order(0) public class GatewayRequestFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { byte[] token = Base64Utils.encode("GATEWAY_TOKEN_VALUE".getBytes()); String[] headerValues = {new String(token)}; ServerHttpRequest build = exchange.getRequest() .mutate() .header("geteway_token", headerValues) .build(); ServerWebExchange newExchange = exchange.mutate().request(build).build(); return chain.filter(newExchange); } }
GateWay actual combat
Source address: https://github.com/langyastudio/langya-tech/tree/master/spring-cloud
Use Nacos Discovery Starter and Spring Cloud Gateway Starter to complete the routing of Spring Cloud services.
- Nacos Alibaba is an open source dynamic service discovery, configuration management and service management platform that is easier to build cloud native applications
- Spring Cloud Gateway It is the official open source library of spring cloud, which can build API gateway on spring MVC
How to access
By modifying the official example nacos-gateway-example To demonstrate the functions of API gateway
Modify POM XML file, introducing Nacos Discovery Starter and Spring Cloud Gateway Starter dependencies
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
Configure the Nacos Server address and Spring Cloud Gateway route in the configuration file
Spring Cloud Gateway can be configured in two ways:
- application.yml profile mode
- Return value of RouteLocator method through @ Bean annotation
spring: main: #Spring cloud gateway is implemented internally through netty + weblux #Weblux implementation and spring boot starter web dependency conflict web-application-type: reactive application: name: nacos-gateway-discovery cloud: #Nacos config nacos: username: nacos password: nacos discovery: server-addr: 127.0.0.1:8848 #spring cloud gateway config gateway: routes: - id: nacos-gateway uri: lb://nacos-discovery-provider # The / nacos of the gateway is mapped to the nacos discovery provider service predicates: - Path=/nacos/** filters: - StripPrefix=1
Use the @ EnableDiscoveryClient annotation to enable service registration and discovery
@SpringBootApplication @EnableDiscoveryClient public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
Service startup
- Start the Nacos discovery provider service
- Start the gateway service of this instance
Execute at this time http://192.168.123.100:18061/nacos/ **The request is actually forwarded to the Nacos discovery provider service, as shown in the following figure:
#Because StripPrefix=1 is used #/ echo/aaa actually forwarded to the Nacos Discovery Provider Service curl 'http://192.168.123.100:18061/nacos/echo/aaa' hello Nacos Discovery aaa
Global exception handling
In spring cloud gateway, DefaultErrorWebExceptionHandler is used by default to handle exceptions. This can be obtained through the configuration class ErrorWebFluxAutoConfiguration.
The default exception handling logic in the DefaultErrorWebExceptionHandler class is as follows:
public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { ... protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse); } ... }
Confirm what resource format is returned according to the request header.
The returned data content is constructed in the DefaultErrorAttributes class.
public class DefaultErrorAttributes implements ErrorAttributes { ... public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap(); errorAttributes.put("timestamp", new Date()); errorAttributes.put("path", request.path()); Throwable error = this.getError(request); MergedAnnotation<ResponseStatus> responseStatusAnnotation = MergedAnnotations.from(error.getClass(), SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class); HttpStatus errorStatus = this.determineHttpStatus(error, responseStatusAnnotation); errorAttributes.put("status", errorStatus.value()); errorAttributes.put("error", errorStatus.getReasonPhrase()); errorAttributes.put("message", this.determineMessage(error, responseStatusAnnotation)); errorAttributes.put("requestId", request.exchange().getRequest().getId()); this.handleException(errorAttributes, this.determineException(error), includeStackTrace); return errorAttributes; } ... }
After reading here, you can see why the above data format is returned. Next, you need to rewrite the return format.
Here, you can customize a CustomErrorWebExceptionHandler class to inherit DefaultErrorWebExceptionHandler, and then modify the logic of generating front-end response data. Then define a configuration class, which can be written with reference to ErrorWebFluxAutoConfiguration. Simply replace the exception class with CustomErrorWebExceptionHandler class.
Please study this method by yourself. It is basically copying code. Rewriting is not complicated. This method will not be demonstrated. Here is another writing method:
Define a global exception class GlobalErrorWebExceptionHandler, let it directly implement the top-level interface ErrorWebExceptionHandler, override the handler() method, and return a custom response class in the handler() method. However, it should be noted that the priority of the rewritten implementation class must be less than that of the built-in ResponseStatusExceptionHandler, which processes the response code of the corresponding error class.
The code is as follows:
/** * Gateway global exception handling */ @Slf4j @Order(-1) @Configuration @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler { private final ObjectMapper objectMapper; @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { ServerHttpResponse response = exchange.getResponse(); if (response.isCommitted()) { return Mono.error(ex); } // Set return JSON response.getHeaders().setContentType(MediaType.APPLICATION_JSON); if (ex instanceof ResponseStatusException) { response.setStatusCode(((ResponseStatusException) ex).getStatus()); } return response.writeWith(Mono.fromSupplier(() -> { DataBufferFactory bufferFactory = response.bufferFactory(); try { //Return response results return bufferFactory.wrap(objectMapper.writeValueAsBytes(ResultData.fail(500,ex.getMessage()))); } catch (JsonProcessingException e) { log.error("Error writing response", ex); return bufferFactory.wrap(new byte[0]); } })); } }
The Privacy Interface prohibits external access
How to prevent the internal privacy interface from being called by the gateway in the spring cloud system? The solutions mainly include:
Blacklist mechanism
Store these interfaces in the "blacklist", read the blacklist configuration when the gateway starts, and then verify whether they are in the blacklist
Interface path
That is, when specifying the access path to the interface, the format is: / access control / interface. Access control can have the following rules (refer to JAVA package specification), which can be extended according to business needs.
pb - public All requests are accessible pt - protected Need to token Access is only possible after authentication pv - private The micro service can only be accessed through the micro gateway df - default Gateway request token Authentication, and request parameters and return results for encryption and decryption ...
With this set of interface specifications, you can flexibly control the access rights of the interface, and then verify the interface path at the gateway. If the corresponding access control rules are hit, the corresponding logic processing will be carried out.
@Component @Order(0) @Slf4j public class GatewayRequestFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //Get request path String rawPath = exchange.getRequest().getURI().getRawPath(); if(isPv(rawPath)){ throw new HttpServerErrorException(HttpStatus.FORBIDDEN,"can't access private API"); } return chain.filter(newExchange); } /** * Determine whether the internal private method * @param requestURI Request path * @return boolean */ private boolean isPv(String requestURI) { return isAccess(requestURI,"/pv"); } /** * Gateway access control verification */ private boolean isAccess(String requestURI, String access) { //The back-end standard request path is / access control / request path int index = requestURI.indexOf(access); return index >= 0 && StringUtils.countOccurrencesOf(requestURI.substring(0,index),"/") < 1; } }
Gray Publishing
- By implementing ServiceInstanceListSupplier to customize the service filtering logic, you can directly inherit DelegatingServiceInstanceListSupplier to implement it
/** * Reference: org springframework. cloud. loadbalancer. core. ZonePreferenceServiceInstanceListSupplier */ @Log4j2 public class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier { public VersionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) { super(delegate); } @Override public Flux<List<ServiceInstance>> get() { return delegate.get(); } @Override public Flux<List<ServiceInstance>> get(Request request) { return delegate.get(request).map(instances -> filteredByVersion(instances,getVersion(request.getContext()))); } /** * filter instance by requestVersion */ private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String requestVersion) { log.info("request version is {}",requestVersion); if(StringUtils.isEmpty(requestVersion)){ return instances; } List<ServiceInstance> filteredInstances = instances.stream() .filter(instance -> requestVersion.equalsIgnoreCase(instance.getMetadata().getOrDefault("version",""))) .collect(Collectors.toList()); if (filteredInstances.size() > 0) { return filteredInstances; } return instances; } private String getVersion(Object requestContext) { if (requestContext == null) { return null; } String version = null; if (requestContext instanceof RequestDataContext) { version = getVersionFromHeader((RequestDataContext) requestContext); } return version; } /** * get version from header */ private String getVersionFromHeader(RequestDataContext context) { if (context.getClientRequest() != null) { HttpHeaders headers = context.getClientRequest().getHeaders(); if (headers != null) { //could extract to the properties return headers.getFirst("version"); } } return null; } }
The implementation principle is the same as the custom load balancing strategy, matching the qualified service instances according to the version.
- Write the configuration class VersionServiceInstanceListSupplierConfiguration to replace the default service instance filtering logic
public class VersionServiceInstanceListSupplierConfiguration { @Bean ServiceInstanceListSupplier serviceInstanceListSupplier(ConfigurableApplicationContext context) { ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder() .withDiscoveryClient() .withCaching() .build(context); return new VersionServiceInstanceListSupplier(delegate); } }
- Use the annotation @ LoadBalancerClient in the gateway startup class to specify which services use the custom load balancing algorithm
Through @ loadbalancerclient (value = "Nacos Discovery Provider", configuration = versionserviceinstancelistsupplierconfiguration. Class), Enable custom load balancing algorithms for Nacos Discovery Provider or through @ LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class) for all services
reference resources
Introduction to routing and forwarding rules of Spring Cloud GateWay
Spring Cloud Gateway: a new generation API gateway service