1.什么是网关
网关是什么?网关(Gateway)本质上不是一个产品而是一个网络层的概念,网关(Gateway)就是一个网络连接到另一个网络的“关口”。简单来说可以理解成火车站的检票口,统一去检票。
2.网关的作用
统一去进行一些操作、处理一些问题。
比如:
- 路由
- 负载均衡
- 统一鉴权
- 跨域
- 统一业务处理(缓存)
- 访问控制
- 发布控制
- 流量染色
- 接口保护
- 限制请求信息
- 脱敏
- 降级(熔断)
- 限流:(比如令牌桶算法、学习漏桶算法)
- 超时时间
- 统一日志
- 统一文档
3.主流网关的对比与选型
网关的分类:
- 全局网关(接入层网关):作用是负载均衡、请求日志等,不和业务逻辑绑定
- 业务网关(微服务网关):会有一些业务逻辑,作用是将请求转发到不同的业务 / 项目 / 接口 / 服务
网关实现方式:
- Nginx(全局网关)、Kong 网关(API 网关,Kong:https://github.com/Kong/kong),编程成本相对高一点
- Spring Cloud Gateway(取代了 Zuul)性能高、可以用 Java 代码来写逻辑,适于学习
主流网关的对比:
我们这里主要介绍springcloudgateway
4.Spring Cloud Gateway 使用方法
Spring Cloud Gateway 官网地址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
核心概念
- 路由(根据什么条件,转发请求到哪里)
- 断言:一组规则、条件,用来确定如何转发路由
- 过滤器:对请求进行一系列的处理,比如添加请求头、添加请求参数
请求流程:
- 客户端发起请求
- Handler Mapping:根据断言,去将请求转发到对应的路由
- Web Handler:处理请求(一层层经过过滤器)
- 实际调用服务
两种配置方式
- 配置式(方便、规范)简化版全称版 √
- 编程式(灵活、相对麻烦)
引入方式:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
开启日志方式(推荐):
在yml配置文件中配置
logging:
level:
org:
springframework:
cloud:
gateway: trace
5.简单示例快速上手具体实现
1.请求转发配置
-
假设对所有路径为:/api/** 的请求进行转发,转发到 http://localhost:8080/api/**
比如请求网关:http://localhost:8090/api/name/get?name=111转发到:
http://localhost:8123/api/name/get?name=111
server:
port: 8090
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=source, genius
routes:
- id: api_route
uri: http://localhost:8080
predicates:
- Path=/api/**
2.编写业务逻辑
使用了 GlobalFilter(编程式),全局请求拦截处理(类似 AOP)
因为网关项目没引入 第三方类库比如MyBatis 等操作数据库的类库,如果该操作较为复杂,可以由增删改查项目提供接口,我们直接调用,不用再重复写逻辑了。
- HTTP 请求(用 HTTPClient、用 RestTemplate、Feign)
- RPC(Dubbo)
因为我们没有引入数据库类库,所以在直接启动application的时候可能会报以下错误:
解决方法:
在启动类加上以下注解
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class})
@Service
public class GeniusApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GeniusApiGatewayApplication.class, args);
}
}
编写过滤器代码:
@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
private static final String INTERFACE_HOST = "http://localhost:8080";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 请求日志
ServerHttpRequest request = exchange.getRequest();
// 打印请求日志
String path = request.getPath().value();
String method = request.getMethod().toString();
log.info("请求唯一标识:" + request.getId());
log.info("请求路径:" + path);
log.info("请求方法:" + method);
log.info("请求参数:" + request.getQueryParams());
String sourceAddress = request.getLocalAddress().getHostString();
log.info("请求来源地址:" + sourceAddress);
log.info("请求来源地址:" + request.getRemoteAddress());
ServerHttpResponse response = exchange.getResponse();
// 2. 访问控制 - 黑白名单
if (!IP_WHITE_LIST.contains(sourceAddress)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
// 3. 简单的用户鉴权
HttpHeaders headers = request.getHeaders();
String accessKey = headers.getFirst("accessKey");
String nonce = headers.getFirst("nonce");
String timestamp = headers.getFirst("timestamp");
String sign = headers.getFirst("sign");
String body = headers.getFirst("body");
if (Long.parseLong(nonce) >10000L){
return handleNoAuth(response);
}
User invokeUser = null;
try {
invokeUser = innerUserService.getInvokeUser(accessKey);
} catch (Exception e) {
log.error("getInvokeUser error", e);
}
if (invokeUser == null) {
return handleNoAuth(response);
}
// 时间和当前时间不能超过 5 分钟
Long currentTime = System.currentTimeMillis() / 1000;
final Long FIVE_MINUTES = 60 * 5L;
if ((currentTime - Long.parseLong(timestamp)) >= FIVE_MINUTES) {
return handleNoAuth(response);
}
return handleResponse(exchange,chain);
}
/**
* 处理响应
*
* @param exchange
* @param chain
* @return
*/
public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerHttpResponse originalResponse = exchange.getResponse();
// 缓存数据的工厂
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
// 拿到响应码
HttpStatus statusCode = originalResponse.getStatusCode();
if (statusCode == HttpStatus.OK) {
// 装饰,增强能力
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
// 等调用完转发的接口后才会执行
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
log.info("body instanceof Flux: {}", (body instanceof Flux));
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
// 往返回值里写数据
// 拼接字符串
return super.writeWith(
fluxBody.map(dataBuffer -> {
// 7. 调用成功,接口调用次数 + 1 invokeCount
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);//释放掉内存
// 构建日志
StringBuilder sb2 = new StringBuilder(200);
List<Object> rspArgs = new ArrayList<>();
rspArgs.add(originalResponse.getStatusCode());
String data = new String(content, StandardCharsets.UTF_8); //data
sb2.append(data);
// 打印日志
log.info("响应结果:" + data);
return bufferFactory.wrap(content);
}));
} else {
// 8. 调用失败,返回一个规范的错误码
log.error("<--- {} 响应code异常", getStatusCode());
}
return super.writeWith(body);
}
};
// 设置 response 对象为装饰过的
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
return chain.filter(exchange); // 降级处理返回数据
} catch (Exception e) {
log.error("网关处理响应异常" + e);
return chain.filter(exchange);
}
}
然后在启动我们8080端口的后端服务
后面访问到网关的请求都会访问到8080端口的服务并且会打印日志信息了√
可以看到我们的日志信息就被打印出来了