服务网关
约 976 字大约 3 分钟
2025-06-30
1.网关
网关:微服务请求的入口,主要负责请求转发、负载均衡、权限校验、日志记录等功能。
举个例子来说,现实中的一个大型的购物中心(类比一个微服务系统),里面有很多不同的商店(各个微服务),比如服装店、餐饮店等等。
假设:整个购物中心没有一个统一的入口,每个商家都有一个独立的入口,这样虽然每个商家都可以独立运营,但是会带来几点坏处,比如难以管理整个购物中心的人流量、统一发放会员优惠券等等
设想:设立一个购物中心统一的入口,游客需要从入口进入之后,根据门口的提示,再前往目的商店
网关的作用:
- 反向代理:接收客户端请求并将其转发到后端服务
- 鉴权:对客户端请求进行身份验证和权限检查
- 流量控制:管控进入系统的请求流量
常见的微服务网关:
- Spring Cloud Netflix 的 Zuul
- Spring Cloud Gateway
- Kong
2.Gateway 的三大核心
- Route:路由,web 前端 通过一些匹配条件,路由定位到真正的服务节点
- Predicate:匹配条件
- Filter:过滤器,以下是过滤器的作用:
- 统计接口的耗时
- 限流
- 设置黑白名单
3.实战
以下是项目管控流式平台的网关配置:
spring:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceld: true
enabled: true
routes:
# 认证中心
- id: project-auth
uri: lb://project-auth
predicates:
- Path=/auth/**
filters:
#验证码处理
- CacheRequestFilter
# 代码生成
- id: project-gen
uri: lb://project-gen
predicates:
- Path=/gen/**
filters:
3.1 路由&断言
其中,对 id 、uri 等进行解释
id :各个服务在 nacos 注册的服务名称
uri :三种配置方式如下
// websocket ws://localhost:9090/ // http http:localhost:9090/ // nacos lb:project-auth
predicates:一组匹配规则,常见有请求 path ,请求参数,请求头
// Path:匹配请求路径 predicates: - Path=/system/**
// Query:匹配请求参数 predicates: - Query=usrname,abc
// Header:匹配具有指定名称的请求头,\d+值 匹配正则表达式 predicates: - Header=X-Request-Id, \d+
3.2 过滤器
网关的过滤器可以分为以下三种:
- 全局过滤器:权限认证、IP 访问限制。需要实现 GlobalFilter 、Ordered 两个接口
- 单一过滤器:作用于单一路由
- 自定义过滤器
以下举例:自定义全局过滤器实现接口调用耗时统计
3.2.1 新建 AuthFilter 类
新建 AuthFilter 类,并实现 GlobalFilter 、Order 接口
3.2.2 实现 filter 方法
主要工作有:
- 白名单过滤
- token 鉴权
- 设置用户信息到请求体
- 接口调用耗时
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
String token = getToken(request);
if (StringUtils.isEmpty(token)) {
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin) {
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
// 设置用户信息到请求
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除(防止网关携带内部请求标识,造成系统安全风险)
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
// Mono.fromRunnable 是非阻塞的,适合在 then 中处理后续的日志逻辑。
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
try {
// 记录接口访问日志
Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
if (beginVisitTime != null) {
URI uri = exchange.getRequest().getURI();
Map<String, Object> logData = new HashMap<>();
logData.put("host", uri.getHost());
logData.put("port", uri.getPort());
logData.put("path", uri.getPath());
logData.put("query", uri.getRawQuery());
logData.put("duration", (System.currentTimeMillis() - beginVisitTime) + "ms");
log.info("访问接口信息: {}", logData);
}
} catch (Exception e) {
log.error("记录日志时发生异常: ", e);
}
}));
}