《SpringCloudGateway结合Sentienl实现网关限流机制》 《Spring Cloud Gateway内置各类型Predicate(断言)使用说明》 《Spring Cloud Gateway过滤器(GatewayFilter)工厂》
1、网关概述
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。
Spring Cloud Gateway 需要 Spring Boot 和 Spring Webflux 提供的 Netty 运行时。它不适用于传统的 Servlet(SpringMVC) 容器。
关键字:
- Route(路由):网关的基本构建块。它由 ID、目标 URI、谓词集合和过滤器集合定义。如果聚合谓词为真,则匹配路由。
- Predicate(断言):匹配来自 HTTP 请求的任何内容,例如:URL、Header、参数等。
- Filter(过滤器):使用特定工厂构建的实例,通过过滤器可以在发送下游请求之前或之后修改请求和响应。
工作流程:
工作流程大体如下:
- 用户向Gateway发送请求
- 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
- 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
- 如果过断言成功,由FilteringWebHandler创建过滤器链并调用
- 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应
2、搭建基础网关服务
使用SpringCloudAlibaba+Nacos 实现Gateway 的整合操作,Nacos搭建参考:
《Docker部署Nacos-2.0.3单机环境》
《Docker部署Nacos-2.0.3集群环境》
《Linux部署Nacos-2.0.3单机环境》
《Linux部署Nacos-2.0.3集群环境》
nacos 整合时需要注意:
- 如果gateway使用namespace,则要和项目中的其他服务的namespace一致。
- 如果gateway使用group,则要和项目中的其他服务的group一致。
2.1 pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2.2 yaml配置
server:
port: 8085
spring:
application:
name: demo-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.0.111:8848
namespace: xxx
group: xxx
gateway:
discovery:
locator:
enabled: true
routes:
- id: user-service
uri: lb://user-service
order: 1
predicates:
- Path=/user/**
转发规则
当请求满足predicates 配置的路径时,会将其请求路径转发到uri路径下,比如:http://127.0.0.1:8085/user/list
上述请求中,请求地址:/user/list满足匹配规则,则会被进行转发,转发后请求为:user-service/user/list ,即会被转发请求user-service服务 ,并且请求路径为/user/list
简化版
如果配置路由的断言匹配规则和服务在注册中心的名称一致时,可以不需要对routes 进行配置,直接通过网关地址/微服务/接口访问即可。
2.3 验证测试:
启动Gateway服务 和service业务服务 ,通过访问接口Gateway 地址,实现请求转发;
为了方便看执行的验证情况,service业务服务 的部分代码(伪代码)如下:
server:
port: 8083
servlet:
context-path: /user
....
....
@RestController
public class UserController {
@GetMapping("list")
public void listUser() {
System.out.println("成功请求user/list接口");
}
}
注册中心服务列表:
直接访问业务服务: 请求地址: http//localhost:8083/user/list 通过Gateway转发到业务服务: 请求地址: http//localhost:8085/user/list 如果通过Gateway服务的地址能够,成功访问到业务服务,则表示Gateway服务搭建成功.
3、Http超时配置
可以为所有路由配置统一的 Http 超时(响应和连接),也可以为某个路由单独配置超时时间(会覆盖全局配置)。
3.1 全局超时配置
connect-timeout :连接超时时长,以毫秒为单位,默认值为45秒。 response-timeout :响应超时时长,以秒为单位,需要加上s ,表示秒。
spring:
cloud:
gateway:
httpclient:
connect-timeout: 10000
response-timeout: 5s
3.2 路由超时
connect-timeout :连接超时时长,以毫秒为单位,默认值为全局配置中的时长。 response-timeout :响应超时时长,以毫秒为单位,默认值为全局配置中的时长。
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
metadata:
response-timeout: 200
connect-timeout: 200
4、跨域配置
注意:如果网关服务进行了跨域配置,那么业务服务就不要再配置跨域,否则会出现冲突导致网关跨域配置失效。
跨域配置有两种方式,通过配置yaml 或者使用@Configuration注解
4.1 方式一:yaml配置
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
4.2 方式二:@Configuration注解
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、全局过滤器
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,日志操作,安全性验证等功能。 1、内置过滤器 SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下: 2、自定义全局过滤器
内置的过滤器已经可以完成大部分的功能,但是还是需要我们自己编写过滤器来实现自定义操作,比如完成统一的权限校验。
代码实现如下:
@Configuration
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
List<String> tokens = headers.get("token");
if (CollectionUtils.isEmpty(tokens)) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
throw new RuntimeException("没有权限");
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
6、请求过滤器与响应过滤器
通过请求过滤器 与响应过滤器 实现对数据的处理,打印请求数据和响应数据等操作,代码如下:
请求过滤器(RequestGlobalFilter):
GatewayContext:
public class GatewayContext {
private String cacheBody;
private MultiValueMap<String, String> formData;
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);
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) {
exchange.getAttributes().put("start", System.currentTimeMillis());
ServerHttpRequest newRequest = exchange.getRequest().mutate().header("CLIENT_IP", ip).build();
exchange = exchange.mutate().request(newRequest).build();
request = exchange.getRequest();
headers = request.getHeaders();
contentType = headers.getContentType();
path = request.getPath().pathWithinApplication().value();
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) {
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;
}
private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
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);
});
ServerHttpRequest mutatedRequest =
new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return ServerRequest.create(mutatedExchange, messageReaders)
.bodyToMono(String.class).doOnNext(objectValue -> {
log.info("MethodParam:{}", objectValue);
gatewayContext.setCacheBody(objectValue);
}).then(chain.filter(mutatedExchange));
});
}
private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) {
return exchange.getFormData()
.doOnNext(multiValueMap -> {
gatewayContext.setFormData(multiValueMap);
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();
if (null == formData || formData.isEmpty()) {
return chain.filter(exchange);
}
StringBuilder formDataBodyBuilder = new StringBuilder();
String entryKey;
List<String> entryValue;
try {
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) {
}
String formDataBodyString = "";
if (formDataBodyBuilder.length() > 0) {
formDataBodyString = formDataBodyBuilder.substring(0,
formDataBodyBuilder.length() - 1);
}
byte[] bodyBytes = formDataBodyString.getBytes(charset);
int contentLength = bodyBytes.length;
ServerHttpRequestDecorator decorator =
new ServerHttpRequestDecorator(
request) {
@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;
}
@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) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
long end = System.currentTimeMillis();
long start = Long.parseLong(exchange.getAttribute("start").toString());
long useTime = end - start;
if (body instanceof Flux) {
String responseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (!StringUtils.isEmpty(responseContentType) && responseContentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
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;
}
}
效果:
7、全局异常处理机制
7.1 编写统一响应类
public class ResponseObject {
private String msg;
private int code;
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;
}
public static ResponseObject success(Object object) {
ResponseObject responseObject = new ResponseObject();
responseObject.setData(object);
responseObject.setCode(200);
responseObject.setMsg("操作成功");
return responseObject;
}
public static ResponseObject success(String msg) {
ResponseObject responseObject = new ResponseObject();
responseObject.setData(true);
responseObject.setCode(200);
responseObject.setMsg(msg);
return responseObject;
}
public static ResponseObject success() {
ResponseObject responseObject = new ResponseObject();
responseObject.setData(true);
responseObject.setCode(200);
responseObject.setMsg("操作成功");
return responseObject;
}
public static ResponseObject success(Object object, String msg) {
ResponseObject responseObject = new ResponseObject();
responseObject.setData(object);
responseObject.setCode(200);
responseObject.setMsg(msg);
return responseObject;
}
public static ResponseObject fail(Object data, String msg) {
ResponseObject responseObject = new ResponseObject();
responseObject.setData(data);
responseObject.setCode(500);
responseObject.setMsg(msg);
return responseObject;
}
public static ResponseObject fail(String msg) {
ResponseObject responseObject = new ResponseObject();
responseObject.setData(false);
responseObject.setCode(400);
responseObject.setMsg(msg);
return responseObject;
}
public static ResponseObject fail() {
ResponseObject responseObject = new ResponseObject();
responseObject.setData(false);
responseObject.setCode(400);
responseObject.setMsg("操作失败");
return responseObject;
}
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 创建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 创建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);
}
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Throwable error = super.getError(request);
return this.buildMessage(request, error);
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return HttpStatus.OK.value();
}
private Map<String, Object> buildMessage(ServerRequest request, Throwable throwable) {
Map<String, Object> map = new HashMap<>(8);
log.error("[网关异常信息]请求路径:{},异常信息:{},异常类型:{}", request.path(), throwable.getMessage(), ExceptionUtils.getStackTrace(throwable));
map.put("message", throwable.getMessage());
map.put("data", false);
return map;
}
}
7.4 修改ResponseGlobalFilter
在ResponseGlobalFilter 中,添加以下代码:
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
....
....
ResponseObject object = JSON.parseObject(responseData, ResponseObject.class);
if (object.getData() != null && object.getCode() != 200 && !(boolean) object.getData())
{
throw new RuntimeException(object.getMsg());
}
....
....
}
7.4 测试
在服务中,抛出异常,测试Gateway 中的处理
@RestController
public class UserController {
@GetMapping("list")
public ResponseObject listUser() {
try {
int a = 1 / 0;
} catch (Exception e) {
return ResponseObject.fail();
}
return null;
}
}
|