网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问
API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施.。主要的功能大概下面这几点
- 性能:API高可用,负载均衡,容错机制。
- 安全:权限身份认证、脱敏,流量清洗,后端签名
- 黑名单。
- 日志:日志记录一旦涉及分布式,全链路跟踪必不可少。
- 缓存:数据缓存。监控:记录请求响应数据,api耗时分析,性能监控。
- 限流:流量控制,错峰流控,可以定义多种限流规则。
- 灰度:线上灰度部署,可以减小风险。
- 路由:动态路由规则。
org.springframework.cloud
spring-cloud-starter-gateway
server:
port: 8087
spring:
profiles:
active: dev
application:
name: nacos-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
因为项目是整合的nacos,所以这里网关的配置也从nacos取
#项目名
spring.application.name=nacos-gateway
#配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
#配置文件尾缀
spring.cloud.nacos.config.file-extension=yml
#配置组
spring.cloud.nacos.config.group=DEV
#命名空间
#spring.cloud.nacos.config.namespace: 7a36049b-ce40-4a3a-97d0-c86858fdcc0f
spring.cloud.nacos.config.refresh-enabled=true
spring.cloud.nacos.config.extension-configs[0].data-id=nacos-gateway-commons.yml
spring.cloud.nacos.config.extension-configs[0].group=DEV
spring.cloud.nacos.config.extension-configs[0].refresh=true
spring.cloud.nacos.config.extension-configs[1].data-id=nacos-gateway-route.yml
spring.cloud.nacos.config.extension-configs[1].group=DEV
spring.cloud.nacos.config.extension-configs[1].refresh=true
这里引入nacos-gateway-route.yml配置如下
这个是网关路由项目的配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: false
#lower-case-service-id: true
routes: #路由数组
- id: nacos-user #路由
uri: lb://nacos-user #lb 指的是负载均衡 后面是nacos的服务名
order: 1 #路由优先级 数字越小越大
predicates: #断言数组(条件判断,返回值boolean ,转发请求满足条件的)
- Path=/nacos-user/**
filters: #过滤器 数组 (在请求传递过程中,对请求做修改)
- StripPrefix=1 #在请求转发之前去掉 gateway的一层路径
- id: nacos-order #路由
uri: lb://nacos-order #lb 指的是负载均衡 后面是nacos的服务名
order: 1 #路由优先级 数字越小越大
predicates: #断言数组(条件判断,返回值boolean ,转发请求满足条件的)
- Path=/nacos-order/**
filters: #过滤器 数组 (在请求传递过程中,对请求做修改)
- StripPrefix=1 #在请求转发之前去掉 gateway的一层路径
这个时候访问
localhost:8083/nacos-user/test会转发到nacos-user应用下的/test接口
localhost:8083/nacos-order/test会转发到nacos-order应用下的/test接口
先给大家看一下这样设计的结果
//请求参数
{
"data": "q9fGLvECb5vDcVh1EKa9Rhf5R56mz6dmRV3G2onljALO7ReKEjqykwBIlL/Jz7wdle7jd03PQhczx0KvrU4JUfMh11YWq5MGkQsid1sENkg=",
"sign": "AD6A2E46B56FF164476B8A441F45AD77"
}
//返回参数
{
"data": "S73h3QYbroWrDnnPfRGKKE/lCOWdpOqVe1hQg2vNuDvdLZPe2uHZ94UNGNIm+tjpJ+caeWoToD53cLi0rwEjCF0jPnOQ4h0lvgAeOP6Ov9StU/yTKDZ+YV3490j8XO5wb367fI5TNyp5cpgWFrw+Y2OAbIDKVCjmUHOpkh3m9zv8frhOEtLyw7bavOsrgKeCh6PS5ND8Mk4pxdjDoD1MFKapIxGFv0X+VktRgYmaSfUR/n/zj3KKxHAC6Ear0MRuDvpVZZEe01OepB9wnkGBdNcL3rqnAQspu5zwUJ7WZS0+g3elsqEgn7Ar7yYK75TgWmlzmCNQv9GKN+V4oG4UW1l7SzLRsD+grHRwaGdFLkHqIYPdAlo1XKXPgHWI/nwS+ANVfliZo/76dAru4d889werCOCyuuxHR1f1ML/7b3TfvQ8NffH2y2ELU9/FvTr0L4pAch20UfNtLosDWkqbrmA7PZewWOf7nwmuBPDoXig0+wSI0s7FFHl+eYuDGEoi3NAAakjYggGFoBmg39KsIZcmIwvZWIP6wVyvkO5PKuN5upfDGgQQtcwlzdryQ+12h74aQXpEnIwotESuBNrZbfi3UNnyEyWE2KguHhhZNUGCKx87Sbb0fFOSbrSOXHr0QGtAQxog4EMQvsqzz8NE494ocjjkdhRSGlwxMLqbgBfap0tHeTqCdFkga7WED3YmOiGRXtjmYL/zBXKSl7ScRyVNLT8ETleVrjOJR7WUQEE=",
"respCode": "0000",
"respDate": "20201226230745",
"respMsg": "请求成功"
}
public class ConstantFilter {
// 设置到 exchange.getAttributes()中的key
public final static String CACHED_REQUEST_BODY_OBJECT_KEY = "CACHED_REQUEST_BODY_OBJECT_KEY";
// 设置到 exchange.getAttributes()中的key
public final static String BG_DEBUG_KEY = "BG_DEBUG";
// 请求参数,返回参数 加密
public final static String REQ_RES_ENCRYPT = "0";
// 请求参出,返回结果 无需加密
public final static String REQ_RES_NALMORE = "1";
}
GlobalCacheRequestBodyFilter:把请求中的body复制到exchange,便于在之后使用。也可以理解为同一个线程中,多个filter之间共享的域
/**
* @calssName AppCacheRequestBodyFilter
* @Description 将 request body 中的内容 copy 一份,记录到 exchange 的一个自定义属性中
* @Author hl
* @DATE 2020/9/27 14:42
*/
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(GlobalCacheRequestBodyFilter.class);
private int order;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("GlobalCacheRequestBodyFilter ...");
// 将 request body 中的内容 copy 一份,记录到 exchange 的一个自定义属性中
Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
// 如果已经缓存过,略过
if (cachedRequestBodyObject != null) {
return chain.filter(exchange);
}
// 如果没有缓存过,获取字节数组存入 exchange 的自定义属性中
return DataBufferUtils.join(exchange.getRequest().getBody())
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
}).defaultIfEmpty(new byte[0])
.doOnNext(bytes -> exchange.getAttributes().put(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, bytes))
.then(chain.filter(exchange));
}
@Override
public int getOrder() {
return this.order;
}
public GlobalCacheRequestBodyFilter(int order){
this.order = order;
}
}
- gateway中我们并不能直接对请求参数进行修改,那么如何才能将加密的请求一解密后的方式路由到下游的服务中去呢?这里我们需要在filter中根据原来的请求对参数解密后创建新的请求。需要注意的是:解密后对请求头CONTENT_LENGTH需要重置,都在会出现读取的参数不完整的情况。
- 如果需要对参数进行解密处理,所以对于客户端的请求统一使用POST的请求方式。
- 加密解密的方法根据你的需要处理。(此处我使用的是RSA,AES结合的方式-之后会继续分享此种加密方式)
public class AppReqDecryptFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(AppReqDecryptFilter.class);
@Autowired
private CheckService checkService;
private int order;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("请求参数系解密...");
// 设置是否加密标识
List bgDebugHeaders = exchange.getRequest().getHeaders().get("bg-debug");
String bgDebug = bgDebugHeaders != null ? bgDebugHeaders.get(0) : ConstantFilter.REQ_RES_ENCRYPT;
exchange.getAttributes().put(ConstantFilter.BG_DEBUG_KEY, bgDebug);
// 获取请求的方法
ServerHttpRequest oldRequest = exchange.getRequest();
String method = oldRequest.getMethodValue();
URI uri = oldRequest.getURI();
if ("POST".equals(method)){
// 尝试从 exchange 的自定义属性中取出缓存到的 body
Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
if (cachedRequestBodyObject != null) {
byte[] decrypBytes;
try {
byte[] body = (byte[]) cachedRequestBodyObject;
String rootData = new String(body); // 客户端传过来的数据
decrypBytes = body;
if(ConstantFilter.REQ_RES_ENCRYPT.equals(bgDebug)) {
//获取密钥
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(Params.AUTHORIZE_TOKEN);
//此处获取自己配置的
SecretIv secretIv = checkService.getKey(exchange, ResType.REQ, token);
logger.info("获取密钥:{}", secretIv);
//报文处理解密
JSONObject data = JSONObject.parseObject(new String(decrypBytes));
Map map = AESUtils.decryptAESRequest(data.getString(Params.AUTHORIZE_DATA), secretIv.getReqSecret(), secretIv.getReqIv());
//验签
checkService.checkSign(map, secretIv.getReqSecret(), data.getString(Params.AUTHORIZE_SIGN));
//下放报文
decrypBytes = JSONHelper.map2json(map).getBytes();
}
}catch (RuntimeException e){
logger.info("客户端数据解析异常:{}", e);
throw new RuntimeException(e.getMessage());
}catch (Exception e){
logger.info("客户端数据解析异常:{}", e);
throw new RuntimeException("系统异常");
}
// 根据解密后的参数重新构建请求
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
Flux bodyFlux = Flux.just(dataBufferFactory.wrap(decrypBytes));
ServerHttpRequest newRequest = oldRequest.mutate().uri(uri).build();
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public Flux getBody() {
return bodyFlux;
}
};
// 构建新的请求头
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
int length = decrypBytes.length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
// headers.set(HttpHeaders.CONTENT_TYPE, "application/json");
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public HttpHeaders getHeaders() {
return headers;
}
};
// 把解密后的数据重置到exchange自定义属性中,在之后的日志GlobalLogFilter从此处获取请求参数打印日志
exchange.getAttributes().put(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, decrypBytes);
return chain.filter(exchange.mutate().request(newRequest).build());
}
}else if("GET".equals(method)){ // todo 暂不处理
Map requestQueryParams = oldRequest.getQueryParams();
return chain.filter(exchange);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return this.order;
}
public AppReqDecryptFilter(int order){
this.order = order;
}
public AppReqDecryptFilter(){
}
public class AppRespEncryptFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(AppReqDecryptFilter.class);
@Autowired
private CheckService checkService;
private int order;
@Autowired
CommonKeys commonKeys;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("返回参数加密...");
String bgDebug = exchange.getAttributeOrDefault(ConstantFilter.BG_DEBUG_KEY, ConstantFilter.REQ_RES_ENCRYPT);
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono writeWith(Publisher extends DataBuffer> body) {
if (body instanceof Flux) {
Flux extends DataBuffer> fluxBody = (Flux extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//释放掉内存
DataBufferUtils.release(join);
// 正常返回的数据
String rootData = new String(content, Charset.forName("UTF-8"));
byte[] respData = rootData.getBytes();
if(ConstantFilter.REQ_RES_ENCRYPT.equals(bgDebug)){
try {
//登录成功的下发密钥
ResultData
如果所有的路由都需要,则自定义过滤器中可以直接实现全局过滤器,就无需配置到指定路由中去了,如:GlobalCacheRequestBodyFilter
@Configuration
public class GatewayConfig {
/**
* 全局过滤器:缓存请求body
*/
@Bean
public GlobalCacheRequestBodyFilter globalCacheRequestBodyFilter(){
return new GlobalCacheRequestBodyFilter(-30);
}
@Bean
public AppRespEncryptFilter globalAppRespEncryptFilter(){
return new AppRespEncryptFilter(-22);
}
@Bean
public AppReqDecryptFilter globalAppReqDecryptFilter(){
return new AppReqDecryptFilter(-25);
}
}