Spring Cloud Alibaba实战(六) - Gateway之鉴权、日志
目录(一)Nacos动态配置(二)Nacos注册中心(三)Sentinel之限流(四)Sentinel之熔断(五)Gateway之路由、限流(六)Gateway之鉴权、日志(七)Gateway搭配Nacos实现动态路由(八)Dubbo + Nacos正文在引入网关后,通常会把每个服务都要做的工作,诸如日志、安全验证等转移到网关处理以减少重复开发。1 加入log4j2这里使用lo...
目录
(一)Nacos动态配置
(二)Nacos注册中心
(三)Sentinel之限流
(四)Sentinel之熔断
(五)Gateway之路由、限流
(六)Gateway之鉴权、日志
(七)Gateway搭配Nacos实现动态路由
(八)Dubbo + Nacos
正文
在引入网关后,通常会把每个服务都要做的工作,诸如日志、安全验证等转移到网关处理以减少重复开发。
1 加入log4j2
这里使用log4j2作为日志组件,首先添加log4j2的依赖并排除SpringBoot默认日志组件的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
在resources目录下创建log4j2-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="1800">
<properties>
<property name="LOG_HOME">D:/Logs/gateway</property>
<property name="REQUEST_FILE_NAME">request</property>
<property name="INFO_FILE_NAME">info</property>
</properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<RollingRandomAccessFile name="info-log"
fileName="${LOG_HOME}/${INFO_FILE_NAME}.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/${INFO_FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
<PatternLayout
pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="100"/>
</RollingRandomAccessFile>
<RollingRandomAccessFile name="request-log"
fileName="${LOG_HOME}/${REQUEST_FILE_NAME}.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/${REQUEST_FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
<PatternLayout
pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="200 MB"/>
</Policies>
<DefaultRolloverStrategy max="200"/>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="info-log" />
</Root>
<Logger name="request" level="info"
additivity="false">
<AppenderRef ref="request-log"/>
</Logger>
<Logger name="org.springframework">
<AppenderRef ref="Console" />
</Logger>
</Loggers>
</Configuration>
在application.yml中增加配置告知log4j2文件路径
logging:
config: classpath:log4j2-spring.xml
2 获取POST的Body
记录日志时通常关注请求URI、Method、QueryString、POST请求的Body、响应信息和来源IP等。对于Spring Cloud Gateway这其中的POST请求的Body获取比较复杂,这里添加一个全局过滤器预先获取并存入请求的Attributes中。
CachePostBodyFilter
@Component
public class CachePostBodyFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String method = serverHttpRequest.getMethodValue();
if("POST".equalsIgnoreCase(method)) {
ServerRequest serverRequest = new DefaultServerRequest(exchange);
Mono<String> bodyToMono = serverRequest.bodyToMono(String.class);
return bodyToMono.flatMap(body -> {
exchange.getAttributes().put("cachedRequestBody", body);
ServerHttpRequest newRequest = new ServerHttpRequestDecorator(serverHttpRequest) {
@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() {
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
DataBuffer bodyDataBuffer = nettyDataBufferFactory.wrap(body.getBytes());
return Flux.just(bodyDataBuffer);
}
};
return chain.filter(exchange.mutate().request(newRequest).build());
});
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -21;
}
}
3 记录日志
接下来再创建一个过滤器用于记录日志
@Component
public class LogFilter implements GlobalFilter, Ordered {
static final Logger logger = LogManager.getLogger("request");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
StringBuilder logBuilder = new StringBuilder();
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String method = serverHttpRequest.getMethodValue().toUpperCase();
logBuilder.append(method).append(",").append(serverHttpRequest.getURI());
if("POST".equals(method)) {
String body = exchange.getAttributeOrDefault("cachedRequestBody", "");
if(StringUtils.isNotBlank(body)) {
logBuilder.append(",body=").append(body);
}
}
ServerHttpResponse serverHttpResponse = exchange.getResponse();
DataBufferFactory bufferFactory = serverHttpResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(serverHttpResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
String resp = new String(content, Charset.forName("UTF-8"));
logBuilder.append(",resp=").append(resp);
logger.info(logBuilder.toString());
byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return -20;
}
}
4 鉴权
对请求的安全验证方案视各自项目需求而定,没有固定的做法,这里仅演示检查签名的处理。规则是:对除sign外所有请求参数按字典顺序排序后组成key1=value1&key2=value2的字符串,然后计算MD5码并与sign参数值比较,一致即认为通过。
这里面同样要处理QueryString和POST方法的Body,因此和日志过滤器合并为在一起。
@Component
public class AuthAndLogFilter implements GlobalFilter, Ordered {
static final Logger logger = LogManager.getLogger("request");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpResponse serverHttpResponse = exchange.getResponse();
StringBuilder logBuilder = new StringBuilder();
Map<String, String> params = parseRequest(exchange, logBuilder);
boolean r = checkSignature(params, serverHttpRequest);
if(!r) {
Map map = new HashMap<>();
map.put("code", 2);
map.put("message", "签名验证失败");
String resp = JSON.toJSONString(map);
logBuilder.append(",resp=").append(resp);
logger.info(logBuilder.toString());
DataBuffer bodyDataBuffer = serverHttpResponse.bufferFactory().wrap(resp.getBytes());
serverHttpResponse.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
return serverHttpResponse.writeWith(Mono.just(bodyDataBuffer));
}
DataBufferFactory bufferFactory = serverHttpResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(serverHttpResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
String resp = new String(content, Charset.forName("UTF-8"));
logBuilder.append(",resp=").append(resp);
logger.info(logBuilder.toString());
byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
private Map<String, String> parseRequest(ServerWebExchange exchange, StringBuilder logBuilder) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String method = serverHttpRequest.getMethodValue().toUpperCase();
logBuilder.append(method).append(",").append(serverHttpRequest.getURI());
MultiValueMap<String, String> query = serverHttpRequest.getQueryParams();
Map<String, String> params = new HashMap<>();
query.forEach((k, v) -> {
params.put(k, v.get(0));
});
if("POST".equals(method)) {
String body = exchange.getAttributeOrDefault("cachedRequestBody", "");
if(StringUtils.isNotBlank(body)) {
logBuilder.append(",body=").append(body);
String[] kvArray = body.split("&");
for (String kv : kvArray) {
if (kv.indexOf("=") >= 0) {
String k = kv.split("=")[0];
String v = kv.split("=")[1];
if(!params.containsKey(k)) {
try {
params.put(k, URLDecoder.decode(v, "UTF-8"));
} catch (UnsupportedEncodingException e) {
}
}
}
}
}
}
return params;
}
private boolean checkSignature(Map<String, String> params, ServerHttpRequest serverHttpRequest) {
String sign = params.get("sign");
if(StringUtils.isBlank(sign)) {
return false;
}
//检查签名
Map<String, String> sorted = new TreeMap<>();
params.forEach( (k, v) -> {
if(!"sign".equals(k)) {
sorted.put(k, v);
}
});
StringBuilder builder = new StringBuilder();
sorted.forEach((k, v) -> {
builder.append(k).append("=").append(v).append("&");
});
String value = builder.toString();
value = value.substring(0, value.length() - 1);
if(!sign.equalsIgnoreCase(MD5Utils.MD5(value))) {
return false;
}
return true;
}
@Override
public int getOrder() {
return -20;
}
}
测试
A:无签名
B:带签名GET请求
C:POST请求
本期源码
链接:https://pan.baidu.com/s/1Vfg9Apnl1OgL8pzeBqHYmw
提取码:jfkl
更多推荐
所有评论(0)