server:
port: 5020
spring:
application:
name: test-gateway
cloud:
nacos:
discovery:
namespace: 645cae0c-cf99-459d-87e1-9a288de8a75j
server-addr: 192.168.0.183:8848
cluster-name: test-platform
service: ${
spring.application.name}
ip: ${
spring.cloud.client.ip-address}
port: ${
server.port}
config:
namespace: 645cae0c-cf99-459d-87e1-9a288de8a75j
server-addr: 192.168.0.183:8848
cluster-name: test-platform
ext-config[0]:
group: gateway-manage
data-id: feign-gateway-${
spring.profiles.active}.yml
ext-config[1]:
group: gateway-manage
data-id: redis-gateway-${
spring.profiles.active}.yml
ext-config[2]:
group: gateway-manage
data-id: routes-${
spring.profiles.active}.yml
ext-config[3]:
group: gateway-manage
data-id: security-${
spring.profiles.active}.yml
refresh: true
ext-config[4]:
group: gateway-manage
data-id: permission-${
spring.profiles.active}.yml
refresh: true
config:
basic:
cors-enabled: false #设置网关的跨域因为是reactive环境 所以basic包中的cors配置无效 需要禁用并单独配置 见CorsConfig.java
time-serializer-enabled: false
security:
h4a.client:
configPath: /home/gateway/config/ #集成海关H4A配置
security-dev.yml
security:
oauth2:
url:
serverUrl: http://test.taylor.net/4a
clientUrl: http://test.taylor.net/api/
client:
#server
userAuthorizationUrl: ${
security.oauth2.url.serverUrl}/oauth/authorize
accessTokenUrl: ${
security.oauth2.url.serverUrl}/oauth/token
ssoLogoutUrl: ${
security.oauth2.url.serverUrl}/logout
refreshTokenUrl: ${
security.oauth2.url.serverUrl}/server/refreshToken
menuTreeUrl: ${
security.oauth2.url.serverUrl}/server/getMenuTree
permissionsUrl: ${
security.oauth2.url.serverUrl}/server/getPermissions
#client
clientId:
- TEST-V3
clientSecret:
- 59b8549f75f9476fbd1e4dccf71c302c
clientUrl:
- http://test.taylor.net
logoutUrl: ${
security.oauth2.url.clientUrl}/logout
redirectUrl:
tokenStoreTime: 1800
signKey: 954c7ca837a446328abe84852245e270
allowUrl: /login,/logout,/ssoLogout
routes-dev.yml
spring:
cloud:
gateway:
default-filters:
- name: StripPrefix
args:
parts: 1
- name: Hystrix
args:
name: default-gateway-fallback
fallbackUri: forward:/fallback
discovery:
locator:
enabled: false
lower-case-service-id: true
routes:
#service1
- id: platform-service1
uri: lb://taylor-platform-service1
predicates:
- Path=/service1/**
#service2
- id: platform-service2
uri: lb://taylor-platform-service2
predicates:
- Path=/service2/**
redis
spring:
redis:
database: 0
password: dev
port: 6379
timeout: 3000ms
jedis:
pool:
max-idle: 8
min-idle: 1
max-active: 8
max-wait: 5000ms
sentinel:
master: mymaster
nodes: 192.168.0.15:26379,192.168.0.15:26479,192.168.0.15:26579
httpclient
http:
config:
connectTimeout: 2000
readTimeout: 2000
writeTimeout: 2000
connectionPool:
maxIdleConnections: 6
keepAliveDuration: 6
feign-gateway
spring.cloud.loadbalancer.retry.enabled: false
#feign.sentinel.enabled: true
feign.hystrix.enabled: true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 8000
ribbon:ConnectTimeout: 7500
ribbon.ReadTimeout: 7500
ribbon.OkToRetryOnAllOperations: false
ribbon.MaxAutoRetriesNextServer: 0
ribbon.MaxAutoRetries: 0
management.endpoints.web.exposure.include: "*"
package com.taylor.gateway.config.cors;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
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.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Objects;
/**
*
* 设置网关的跨域
* 因为是reactive环境 所以basic包中的cors配置无效 需要禁用并单独配置
*/
@Configuration
public class CorsConfig implements WebFilter, Ordered {
private static final String MAX_AGE = "1800";
@Override
public int getOrder() {
return -30;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(exchange);
}
HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = exchange.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
if (Objects.nonNull(requestMethod)) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, CorsConfiguration.ALL);
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(exchange);
}
}
FallbackHandler
package com.taylor.gateway.config.exception;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
* 处理请求路由的熔断降级
*
*
*/
@RestController
public class FallbackHandler {
// @RequestMapping("/fallback")
// public Mono fallback() {
// log.warn("===> Gateway timeout [{} {} {}]");
// return Mono.just(Result.build(Status.FAILURE, "网关处理超时"));
// }
/**
* 请求超时会执行降级 但是没有可用的服务实例时也会进入降级
* 为了在日志中更精确的区分是超时还是服务不可用 在降级方法中抛出异常 交由ErrorHandler去处理
*/
@RequestMapping("/fallback")
public void fallback() {
throw new FallbackException("当前请求无法完成处理,请稍后再试");
}
}
ErrorWebExceptionHandler 配置
package com.taylor.gateway.config.error;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import java.util.Collections;
import java.util.List;
/**
*
* 覆盖默认的错误处理类
*/
@Configuration
@EnableConfigurationProperties({
ServerProperties.class, ResourceProperties.class})
public class ErrorHandlerConfig {
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public ErrorHandlerConfig(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) {
ErrorWebExceptionHandler exceptionHandler = new ErrorWebExceptionHandler(
errorAttributes,
this.resourceProperties,
this.serverProperties.getError(),
this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}
package com.taylor.gateway.config.error;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import com.netflix.hystrix.exception.HystrixTimeoutException;
import com.taylor.gateway.constant.ServerRequestConstant;
import com.taylor.gateway.record.AccessRecord;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.cloud.gateway.support.TimeoutException;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
*
* 处理请求路由过程中的异常
*
*/
@Slf4j
public class ErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
public ErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
//获取错误信息
Throwable throwable = super.getError(request);
//计算请求开始时间
Optional<Object> optional = request.attribute(ServerRequestConstant.REQUEST_TIME_BEGIN);
LocalDateTime startTime = optional.isPresent() ? LocalDateTime
.ofEpochSecond((Long) optional.get() / 1000, 0, ZoneOffset.ofHours(8))
: LocalDateTime.now();
//请求信息记录
AccessRecord record = new AccessRecord(request);
record.setStartTime(startTime);
record.setHttpStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
record.setResult(throwable.getMessage() == null ? throwable.toString() : throwable.getMessage());
record.setEndTime(LocalDateTime.now());
//此处的状态只表示gateway
//比如 此处的404是gateway找不到路由mapping时的状态 而不是具体服务实例返回的状态
if(throwable instanceof ResponseStatusException) {
HttpStatus status = ((ResponseStatusException) throwable).getStatus();
record.setHttpStatus(status.value());
if(status == HttpStatus.NOT_FOUND) {
return buildResult(status.value(), "未知的请求路径");
} else if (status == HttpStatus.BAD_REQUEST) {
return buildResult(status.value(), Objects.isNull(throwable.getMessage())
? "请求无法进行处理" : throwable.getMessage());
}
return buildResult(status.value(), "请求处理失败");
}
//是否是超时类异常
boolean rs = (throwable instanceof HystrixRuntimeException
&& throwable.getCause() instanceof HystrixTimeoutException)
|| throwable instanceof TimeoutException;
if(rs) {
record.setResult("Read timed out");
return buildResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), "请求处理超时");
}
//其它类型的异常统一处理
return buildResult(HttpStatus.INTERNAL_SERVER_ERROR.value(),
throwable.getMessage() == null ? "网关处理发生错误" : throwable.getMessage());
}
@Override
protected void logError(ServerRequest request, HttpStatus errorStatus) {
Throwable throwable = super.getError(request);
//如果是找不到路由 则打印日志 不打印栈信息
if(errorStatus == HttpStatus.NOT_FOUND) {
log.warn(buildMessage(request, throwable), throwable);
return;
}
//熔断类和超时类异常也不打印栈信息
if(throwable instanceof HystrixRuntimeException || throwable instanceof TimeoutException) {
Throwable cause = throwable.getCause();
log.error(buildMessage(request, cause == null ? new TimeoutException("Read timed out") : cause), throwable);
return;
}
//其它类型异常统一打印
log.error(buildMessage(request, throwable), throwable);
}
private Map<String, Object> buildResult(int status, String msg) {
Map<String, Object> params = new HashMap<>(4);
params.put("status", status);
params.put("msg", msg);
params.put("page", null);
params.put("data", null);
return params;
}
private String buildMessage(ServerRequest request, Throwable ex) {
//此处可以增加打印其它的信息
//因为webflux本身API的问题 暂时没有办法获取到IP地址
StringBuilder message = new StringBuilder("Failed to handle request [");
message.append(request.methodName());
message.append(" ");
message.append(request.uri());
message.append("]");
if(ex != null) {
message.append(": ");
message.append(ex.getMessage() == null ? ex.toString() : ex.getMessage());
}
return message.toString();
}
}
package com.taylor.gateway.record;
import com.alibaba.fastjson.JSON;
import com.taylor.gateway.constant.ServerRequestConstant;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.reactive.function.server.ServerRequest;
import java.time.LocalDateTime;
/**
*
* 用于记录网关的请求
*
*/
@Getter
@Setter
@ToString
@NoArgsConstructor
public class AccessRecord {
private String ipAddress;
private String url;
private String method;
private String headers;
private String bodyParams;
private String queryParams;
private Integer httpStatus;
private String result;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Long millisecond;
private String exception;
public AccessRecord(ServerRequest request) {
this.ipAddress = (String) request.attribute(ServerRequestConstant.ADDRESS_HEADER).orElse("IP Address is Null");
this.url = request.uri().toString();
this.method = request.methodName();
this.headers = JSON.toJSONString(request.headers().asHttpHeaders());
this.queryParams = JSON.toJSONString(request.queryParams());
}
public AccessRecord(ServerHttpRequest request) {
this.url = request.getURI().toString();
this.method = request.getMethodValue();
this.headers = JSON.toJSONString(request.getHeaders());
this.queryParams = JSON.toJSONString(request.getQueryParams());
}
}
package com.taylor.gateway.constant;
/**
*
* HTTP相关常量
*
*/
public class ServerRequestConstant {
/**
* 请求开始时间KEY
*/
public static final String REQUEST_TIME_BEGIN = "Request-Start-Time";
/**
* 请求客户端的IP地址HEAD
*/
public static final String ADDRESS_HEADER = "Client-IP-Address";
/**
* 请求协议
*/
public static final String HTTP_HEAD = "HTTP";
/**
* 请求协议
*/
public static final String HTTPS_HEAD = "HTTPS";
/**
* 请求方式
*/
public static final String REQUEST_METHOD_POST = "POST";
}
webflux暂不支持mysql 因此只记录在日志中
直到 Spring Boot 2.3.0.RELEASE,才正式支持基于 r2dbc 的 MySQL 驱动。后续有空再更新 https://www.jianshu.com/p/28bec31ae72c
package com.taylor.gateway.filter;
import com.taylor.gateway.constant.ServerRequestConstant;
import com.taylor.gateway.record.AccessRecord;
import com.taylor.gateway.record.Records;
import com.taylor.gateway.util.ServerUtil;
import com.taylor.gateway.util.StringUtil;
import io.netty.buffer.UnpooledByteBufAllocator;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.DefaultServerRequest;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Objects;
import java.util.UUID;
/**
*
* 记录网关的请求日志信息
*/
@Slf4j
@Component
public class LoggerGlobalFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return -5;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//验证类型
String scheme = exchange.getRequest().getURI().getScheme().toUpperCase();
boolean rs = ServerRequestConstant.HTTP_HEAD.equals(scheme) || ServerRequestConstant.HTTPS_HEAD.equals(scheme);
if(!rs) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
String ipAddress = ServerUtil.ipAddress(request);
//存放请求相关信息
exchange.getAttributes().put(ServerRequestConstant.REQUEST_TIME_BEGIN, System.currentTimeMillis());
exchange.getAttributes().put(ServerRequestConstant.ADDRESS_HEADER, ipAddress);
//记录请求的基本信息
AccessRecord record = new AccessRecord(request);
record.setStartTime(LocalDateTime.now());
record.setIpAddress(ipAddress);
//记录请求体的类型 上传文件类型的请求单独处理
//GET请求不存在请求体 只记录POST类型
// TODO: 2019/12/18 需要支持MULTIPART_FORM_DATA的处理
String contentType = request.getHeaders().getFirst("Content-Type");
boolean res =
Objects.nonNull(contentType)
&& ServerRequestConstant.REQUEST_METHOD_POST.equals(request.getMethodValue())
&& !contentType.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE);
if(res) {
//POST方式 读取请求体
//requestBody具有只能被读取一次的特性 所以需要在记录之后重新包装一个新的request对象
//因为gateway是基于reactive开发的原因
//导致遇到很多问题 目前的实现方式能够避免一下问题 暂不知后续是否还有问题
//1.请求体只能读取一次
//2.请求体超过1024B被截断
//3.请求体为空时无法获取到响应
ServerHttpRequest mutatedRequest = resolveRequest(exchange, record);
ServerHttpResponse mutatedResponse = resolveResponse(exchange, record);
return filter(exchange.mutate().request(mutatedRequest).response(mutatedResponse).build(), chain, record);
}
//GET方式直接使用原有对象
ServerHttpResponse mutatedResponse = resolveResponse(exchange, record);
return filter(exchange.mutate().response(mutatedResponse).build(), chain, record);
}
/**
* 记录响应之后的耗时信息
* 处理访问请求记录
* @param exchange
* @param chain
* @return
*/
private Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain, AccessRecord record) {
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(ServerRequestConstant.REQUEST_TIME_BEGIN);
//计算耗时
//如果exchange中没有获取到REQUEST_TIME_BEGIN 则使用记录中的开始时间
if(Objects.isNull(startTime)) {
//转换为毫秒数
startTime = record.getStartTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
//为了防止输出的日志太多 此处只打印部分日志
//同时添加一个ID 后续可以通过此ID获取完整的记录
String recordId = UUID.randomUUID().toString().replaceAll("-", "");
record.setException(recordId);
log.error("===> {} {} {} request attribute[requestTimeBegin] not found",
record.getIpAddress(), record.getUrl(), recordId);
}
record.setEndTime(LocalDateTime.now());
record.setMillisecond(System.currentTimeMillis() - startTime);
if (exchange.getResponse().getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
//限流请求
record.setHttpStatus(HttpStatus.TOO_MANY_REQUESTS.value());
record.setResult("操作太频繁, 触发请求限流");
}
//处理请求记录
Records.handler(record);
})
);
}
/**
* 处理请求体
* 处理之后封装为新的request
* @param exchange
* @param record
* @return
*/
private ServerHttpRequest resolveRequest(ServerWebExchange exchange, AccessRecord record) {
DefaultServerRequest serverRequest = new DefaultServerRequest(exchange);
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
//记录请求体内容
private StringBuilder builder = new StringBuilder();
//因为没有对请求体进行操作 所以这里重写只是为了加上TRANSFER_ENCODING的请求头
//TRANSFER_ENCODING是用来表示分块传输 目前不知道具体意义
//因为读取方式是参考的源码类ModifyRequestBodyGatewayFilterFactory
//此类也进行了重写 为了防止未知问题
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
//解析请求体 能够解决请求体过长获取不完整的问题
//使用此方式 在请求体为空时 依然能截获到响应
@Override
public Flux<DataBuffer> getBody() {
return serverRequest.bodyToFlux(String.class).map(body -> {
builder.append(body);
NettyDataBufferFactory nettyDataBufferFactory =
new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
return nettyDataBufferFactory.wrap(body.getBytes(StandardCharsets.UTF_8));
}).doOnComplete(()-> record.setBodyParams(StringUtil.format(builder.toString())));
}
};
return mutatedRequest;
}
/**
* 处理并读取response
* 读取之后需要封装为新的response
* @param exchange
* @param record
* @return
*/
private ServerHttpResponse resolveResponse(ServerWebExchange exchange, AccessRecord record) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
ServerHttpResponseDecorator mutatedResponse = new ServerHttpResponseDecorator(response) {
//记录响应体内容
//应该也会出现响应体过长导致的截断问题
//测试结果 当响应体过长时 会多次读取 前端收到的结果是完整的 记录日志时 需要多次append才能得到完整的数据
//记录响应体内容
private StringBuilder builder = new StringBuilder();
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
//记录请求执行状态
//只能写在读取方法中 因为此时才收到了响应
record.setHttpStatus(response.getStatusCode() != null ? response.getStatusCode().value() : null);
/*************************************************/
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
String resp = new String(bytes, StandardCharsets.UTF_8);
builder.append(resp);
record.setResult(StringUtil.format(builder.toString()));
return bufferFactory.wrap(bytes);
}));
}
/*************************************************/
return super.writeWith(body);
}
};
return mutatedResponse;
}
}
三个辅助类
package com.taylor.gateway.record;
import lombok.extern.slf4j.Slf4j;
/**
*
* 请求访问记录处理 todo Spring Boot 2.3.0.RELEASE,正式支持基于 r2dbc 的 MySQL 驱动
*/
@Slf4j
public class Records {
public static void handler(AccessRecord record) {
log.info(record.toString());
}
}
package com.taylor.gateway.util;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
*
* 字符串相关操作
*/
public class StringUtil {
private static final Pattern PATTERN = Pattern.compile("\\s*|\t|\r|\n");
/**
* 去掉空格,换行和制表符
* @param str
* @return
*/
public static String format(String str){
if (!StringUtils.isEmpty(str)) {
Matcher m = PATTERN.matcher(str);
return m.replaceAll("");
}
return str;
}
/**
* 去除权限信息中重复的数据
* @param auth
* @return
*/
public static String deduplication(String auth) {
String[] authArr = auth.split(",");
return Stream.of(authArr).distinct().collect(Collectors.joining(","));
}
}
package com.taylor.gateway.util;
import io.netty.buffer.ByteBufAllocator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
*
* 获取ServerHttpRequest中的信息
*
*/
public class ServerUtil {
private static final String HEADER_VALUE_UNKNOWN = "UNKNOWN";
private ServerUtil(){
}
/**
* webflux的request用此方式获取
* 从header中获取 通过代理访问后使用默认api获取的地址不准确
* @param request
* @return
*/
public static String ipAddress(ServerHttpRequest request) {
// InetSocketAddress remoteAddress = request.getRemoteAddress();
// if(Objects.isNull(remoteAddress)) {
// return "remoteAddress is Null";
// }
// String hostAddress = remoteAddress.getAddress().getHostAddress();
// return hostAddress;
HttpHeaders headers = request.getHeaders();
String ipVal = headers.getFirst("x-forwarded-for");
if (StringUtils.isNotEmpty(ipVal) && ! HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
boolean isContains = ipVal.contains(",");
if (isContains) {
ipVal = ipVal.split(",")[0];
}
return ipVal;
}
if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
ipVal = headers.getFirst("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
ipVal = headers.getFirst("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
ipVal = headers.getFirst("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
ipVal = headers.getFirst("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
ipVal = headers.getFirst("X-Real-IP");
}
if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
ipVal = request.getRemoteAddress().getAddress().getHostAddress();
}
return ipVal;
}
/**
* 读取request中的请求体
* @param request
* @return
*/
public static String resolve(ServerHttpRequest request) {
StringBuilder sb = new StringBuilder();
request.getBody().subscribe(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
String body = new String(bytes, StandardCharsets.UTF_8);
sb.append(body);
});
return sb.toString();
}
/**
* 还原body为DataBuffer
* @param body
* @return
*/
public static DataBuffer restore(String body) {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
/**
* 使用response写出信息
* @param response
* @param msg
* @return
*/
public static Mono<Void> write(ServerHttpResponse response, String msg) {
return write(response, HttpStatus.OK, msg);
}
/**
* 使用response写出信息
* @param response
* @param msg
* @return
*/
public static Mono<Void> write(ServerHttpResponse response, HttpStatus status, String msg) {
int value = status.value();
response.setStatusCode(status);
return response.writeWith(Flux.just(ServerUtil.restore(ResultUtil.buildResultJSON(value, msg))));
}
}
package com.taylor.gateway.config.ratelimit;
import com.taylor.gateway.util.ServerUtil;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
*
* 根据请求IP进行限流
*
*/
@Configuration
public class HostKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(ServerUtil.ipAddress(exchange.getRequest()));
}
}
package com.taylor.gateway.config.redis;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Created by xxon 2017/11/3.
*
* redis配置
*/
@Configuration
public class RedisConfig {
@Bean("redisTemplate")
@SuppressWarnings("unchecked")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new FastJsonRedisSerializer(Object.class));
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}