实现注册服务接口返回统一处理
由于前后端分离,后台微服务较多,很有可能在一些接口中会出现异常,及参数返回错误等情况,则需要网关统一对该类异常进行捕获及处理
本人实现功能:
1.前端传递参数解密处理
2.微服务响应参数处理
后期会在网关层对返回类进行其他处理,比如加密、生成标签、摘要等,之后会继续补充
一、前端参数过滤及解密处理
1.通过gateWay定义过滤器实行访问拦截
2.进行对应的解密处理
gateWay配置代码:
spring:
redis:
host: 127.0.0.1
port: 6379
password:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 认证中心
- id: test-auth
uri: lb://test-auth # 环境部署启用了负载均衡访问
predicates:
- Path=/auth/** # 路径相匹配进行路由跳转
filters:
# 验证码处理
- ValidateCodeFilter
- StripPrefix=1
#其他微服务配置
- id: test-111
uri: lb://test-111
predicates:
- Path=/code/**
filters:
- StripPrefix=1
1、由于前端只对auth服务下的请求进行了加密,因此Path处只拦截了auth下的请求,该微服务名为test-auth
2、filters处即为我们在gateWay定义的过滤器ValidateCodeFilter(最开始是用来校验验证码的,被我直接复制过来的)
代码如下:
package com.pldw.gateway.filter;
import com.pldw.auth.api.RemoteAuthService;
import com.pldw.common.core.exception.CaptchaException;
import com.pldw.common.exception.constant.PldwHttpStatus;
import com.pldw.common.exception.domain.PldwResult;
import com.pldw.gateway.service.AuthorizationService;
import com.pldw.gateway.util.AutowiredBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
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 org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.pldw.common.core.utils.StringUtils;
import com.pldw.gateway.service.ValidateCodeService;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.net.URLDecoder;
import java.util.Iterator;
import java.util.Map;
/**
* 验证码过滤器
*
* @author pldw
*/
@Component
public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object> implements GatewayFilter, Ordered
{
RemoteAuthService remoteAuthService;
private final static String AUTH_URL = "/oauth/token";
@Autowired
private ValidateCodeService validateCodeService;
@Autowired
private AuthorizationService authorizationService;
private static final String BASIC_ = "Basic ";
private static final String CODE = "code";
private static final String UUID = "uuid";
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final String KEY = "pldwzxpt";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//这里暂未处理
//RemoteAuthService jupyterAuthService = AutowiredBean.getBean(RemoteAuthService.class);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 100;
}
@Override
public GatewayFilter apply(Object config)
{
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
try {
//1用户token校验方法(不需要注意)
if (!StringUtils.isEmpty(header)){
authorizationService.checkToken(header,request.getURI().getPath());
}
else
{
//2、登录请求验证码校验(不需要注意) =======================================================
validateCodeService.checkCapcha(request.getQueryParams().getFirst(CODE),request.getQueryParams().getFirst(UUID)
);
//3、登录则解密处理=========================================================================
if (StringUtils.containsIgnoreCase(request.getURI().getPath(), AUTH_URL))
{
//账号密码解密
if (request.getQueryParams().containsKey(USERNAME)||request.getQueryParams().containsKey(PASSWORD)) {
MultiValueMap<String, String> multiValueMap = request.getQueryParams();
Iterator<Map.Entry<String, String>> it = multiValueMap.toSingleValueMap().entrySet().iterator();
StringBuilder query = new StringBuilder();
String username = "";
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
if (entry.getKey().equals("password")) {
try {
//该处为RSA的解密方法,根据需要替换掉你需要的解密方法即可(参数为需解密字符串,解密用户名),此方法我是通过用户名来对应公私钥,所以参数需要有用户名,具体RSA非对称加密后期会详细介绍
String decodeCode = validateCodeService.getDecodeCode(URLDecoder.decode(entry.getValue()),username);
query.append(entry.getKey()).append("=").append(decodeCode).append("&");
} catch (Exception e) {
throw new CaptchaException("用户名或密码错误!");
}
} else {
query.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
MultiValueMap<String, String> mu = request.getQueryParams();
URI uri2 = request.getURI();
URI newUri = UriComponentsBuilder.fromUri(uri2)
.replaceQuery(query.toString())
.build(true)
.toUri();
request = request.mutate().uri(newUri).build();
exchange = exchange.mutate().request(request).build();
}
}
return chain.filter(exchange);
};
}
catch (Exception e)
{
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return exchange.getResponse().writeWith(
Mono.just(response.bufferFactory().wrap(
JSON.toJSONBytes(
PldwResult.error("参数解密异常!")
)
)
)
);
}
return chain.filter(exchange);
};
}
}
1、其中getOrder为定义加载顺序,百度了很多,感觉解释很费劲,都说是数值最小的最先加载,数值低的后加载,比如想先加载就可以用0,-1,想最后加载可以用100这样较大的数,这里最后加载是防止先加载调用对象无法调用问题
2、validateCodeService是我自己定义的对应处理方法,包含校验用户账户密码,验证码是否为空、正确及相关解密方法调用
3、UriComponentsBuilder那一段代码是为了修改前端传递参数,重新定义为我们网关处理后的参数放入请求中,这样访问其他微服务时候参数即为处理后参数
4、最后捕获异常中的writeWith等代码为异常返回处理,如果在接收到异常参数或者解密出现问题时会直接返回处理后的异常,此处的PldwResult对象其实就是一个MAP,可以根据自己需要定义
二、微服务响应参数处理
通过过滤器对微服务接口返回值进行统一处理
做的时候找过较多方法,最后发现都大同小异,总结出来,还是直接将返回json处理后重新赋值返回较好处理,代码如下
package com.pldw.gateway.filter;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.pldw.common.core.constant.CacheConstants;
import com.pldw.common.exception.constant.PldwHttpStatus;
import com.pldw.common.exception.domain.ApiResult;
import net.sf.json.JSONObject;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
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.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
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 lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 网关响应处理器
* @author cl
* **/
@Component
@Slf4j
public class WrapperResponseGlobalFilter implements GlobalFilter, Ordered{
/**用于拼接字符串 **/
private static Joiner joiner = Joiner.on("");
private static String exportList = "exportList";
private static String feignApi = "feignTest";
@Override
public int getOrder() {
return -2;//要最先加载
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().toString();
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
ServerHttpResponseDecorator decorator = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux && !url.endsWith(exportList)) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>)body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBuffer join = bufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String responseData = new String(content, StandardCharsets.UTF_8);
// 重置返回参数
String result = response(responseData,url);
byte[] uppedContent =
new String(result.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8).getBytes();
// 修改后的返回参数应该重置长度,否则如果修改后的参数长度超出原始参数长度时会导致客户端接收到的参数丢失一部分
response.getHeaders().setContentLength(uppedContent.length);
response.setStatusCode(HttpStatus.OK);
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decorator).build());
}
private String response(String result,String url) {
//文件流,后台调用接口不处理
if(!url.contains(feignTest)&&!url.contains(CacheConstants.zxptLoginKey)) {
//对接口返回值处理
try {
JSONObject jsonObject = JSONObject.fromObject(result);
ApiResult pldwResult =JSON.parseObject(jsonObject.toString(), ApiResult.class);
//返回参数不规范时转化为对应规范参数
if (pldwResult.getResCode() == null) {
ApiResult tempResult = ApiResult.success(jsonObject);
result = JSONArray.toJSON(tempResult).toString();
}
} catch (Exception e) {
Map hashMap = new HashMap();
hashMap.put("code", PldwHttpStatus.customException + PldwHttpStatus.DDSuccess + PldwHttpStatus.DDSystemException);
ApiResult pldwResult = ApiResult.success("系统内部错误", hashMap,Map.class);
result = JSONArray.toJSON(pldwResult).toString();
}
}
return result;
}
}
实际过程中遇到了Bug,由于通过list分段存储后会导致byte[]数组被分割,中文部分可能会有部分乱码,比如你好,转为byte[]成了"12345678",然后再转为String时候放入list被分割为了两段list[1]=“12345”,list[2]="678"字符串显然是解析不回中文的,因此我改为了直接全部读取,原代码为
package com.pldw.gateway.filter;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.pldw.common.core.constant.CacheConstants;
import com.pldw.common.exception.constant.PldwHttpStatus;
import com.pldw.common.exception.domain.ApiResult;
import net.sf.json.JSONObject;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
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.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
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 lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 网关响应处理器
* @author cl
* **/
@Component
@Slf4j
public class WrapperResponseGlobalFilter implements GlobalFilter, Ordered{
/**用于拼接字符串 **/
private static Joiner joiner = Joiner.on("");
private static String exportList = "exportList";
private static String feignApi = "feignTest";
@Override
public int getOrder() {
return -2;//要最先加载
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().toString();
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
ServerHttpResponseDecorator decorator = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux && !url.endsWith(exportList)) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>)body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
//↓====↓====↓====↓====↓=原有Bug部分代码=↓====↓====↓====↓====↓====↓===↓
List<String> list = Lists.newArrayList();
// gateway 针对返回参数过长的情况下会分段返回,使用如下方式可能会造成中文乱码
dataBuffers.forEach(dataBuffer -> {
// probably should reuse buffers
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
// 释放掉内存
DataBufferUtils.release(dataBuffer);
list.add(new String(content, StandardCharsets.UTF_8));
});
// 将多次返回的参数拼接起来
String responseData = joiner.join(list);
//↑======↑====↑====↑===↑=原有Bug部分代码=↑====↑====↑====↑===↑====↑====↑
// 重置返回参数
String result = response(responseData,url);
byte[] uppedContent =
new String(result.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8).getBytes();
// 修改后的返回参数应该重置长度,否则如果修改后的参数长度超出原始参数长度时会导致客户端接收到的参数丢失一部分
response.getHeaders().setContentLength(uppedContent.length);
response.setStatusCode(HttpStatus.OK);
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decorator).build());
}
private String response(String result,String url) {
//文件流,后台调用接口不处理
if(!url.contains(feignTest)&&!url.contains(CacheConstants.zxptLoginKey)) {
//对接口返回值处理
try {
JSONObject jsonObject = JSONObject.fromObject(result);
ApiResult pldwResult =JSON.parseObject(jsonObject.toString(), ApiResult.class);
//返回参数不规范时转化为对应规范参数
if (pldwResult.getResCode() == null) {
ApiResult tempResult = ApiResult.success(jsonObject);
result = JSONArray.toJSON(tempResult).toString();
}
} catch (Exception e) {
Map hashMap = new HashMap();
hashMap.put("code", PldwHttpStatus.customException + PldwHttpStatus.DDSuccess + PldwHttpStatus.DDSystemException);
ApiResult pldwResult = ApiResult.success("系统内部错误", hashMap,Map.class);
result = JSONArray.toJSON(pldwResult).toString();
}
}
return result;
}
}
1、ServerHttpResponseDecorator中的方法我也是网上找的,主要是实现读取返回JSON字符串,最后使用responseData 接收,然后该字符串即为调用其他微服务接口后重新返回网关的json值(其余微服务未直接返回页面及重定向,均返回Json字符串)
2、代码处endsWith(exportList)不处理返回json是因为包含该路径接口均为导出接口,返回值为输出流,一旦再拼接无法正常下载
3、response(String result,String url)方法即为自定义返回值处理,正常情况下可直接返回原接口定义Json,若需要可添加判断,根据需要来,json字符串都有了,什么加密、返回值封装时间戳什么外套的,岂不是能为所欲为
三、微服务连接异常捕获
直接上方法,放入网关代码中即可
package com.puldw.cep.gateway.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.puldw.common.exception.constant.PldwHttpStatus;
import com.puldw.common.exception.domain.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;
@Component
@Primary
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class ServiceNoFoundExceptionHandler implements WebExceptionHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
@SuppressWarnings("NullableProblems")
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
ApiResult vo;//我自定义的返回类,可根据自己需要定义返回值
if (ex instanceof ResponseStatusException && ((ResponseStatusException) ex).getStatus() == HttpStatus.NOT_FOUND) {
vo = ApiResult.error(PldwHttpStatus.connectConnectException+"00"+PldwHttpStatus.DDSystemException,"访问地址不存在");
} else {
log.info(ex.getMessage());
vo = ApiResult.error(PldwHttpStatus.connectConnectException+"00"+PldwHttpStatus.DDSystemException,"访问失败,请稍后再试");
}
return response.writeWith(Mono.fromSupplier(() -> {
byte[] bytes;
try {
bytes = objectMapper.writeValueAsBytes(vo);
} catch (JsonProcessingException e) {
response.setStatusCode(HttpStatus.OK);
bytes = "系统内部错误".getBytes();
}
return response.bufferFactory().wrap(bytes);
}));
}
}