122
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | 2302软件工程社区 |
|---|---|
| 这个作业要求在哪里 | 软件工程实践总结&个人技术博客 |
| 这个作业的目标 | 个人技术总结 |
| 其他参考文献 | 《构建之法》 |
Spring Cloud Gateway是建立在Spring生态系统之上的API网关,用于构建基于Spring框架的微服务网关,用于路由请求、执行过滤器链等。通常用于微服务架构中统一处理请求、提供安全性、监控等功能。学习该技术可提高微服务架构的可伸缩性和灵活性。难点在于配置路由规则、理解过滤器链的执行顺序等。
SpringCloud是基于Spring生态的网关服务,首先项目中需要有Spring依赖(Boot/Cloud),然后我们在网关模块引入Spring Cloud Gateway的相关依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
server:
port: 10001 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: nacos-server:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user # 路由id, 自定义,唯一即可
# uri: 127.0.0.1:/user # - 路由目的地,支持lb和http两种
uri: lb://userService # 路由的目的地,lb是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断是否符合路由规则的条件
- Path=/user-service/** # path 按照路径进行匹配,只要以/user-service/开头就符合规则
filters:
- StripPrefix=1 # 过滤器,去除请求的前缀路径,StripPrefix=1 表示去除请求路径的第一个路径片段
当然,手动指定路由有些麻烦,每次新编写一个服务都需要来网关模块手动配置路由,我们可以根据SpringCloud的服务注册与发现功能,简简单单的从注册中心获取服务,并自动生成路由,只要使用修改使用以下配置即可,我们这里以nacos为例:
网关路由可以配置的内容有:
做到这里,一个简易的路由转发网关服务就搭建成功了,直接运行GatewayApplication服务,得到结果如图:

接下来只要请求/user-service/xx,网关就会将路由转发到userService服务的对应接口上
要想要让微服务们,相互通信,我们需要实现一个服务注册与发现的机制。一个分布式系统中,服务的部署和扩展是动态变化的。我们通过服务注册与发现可以将服务注册到服务注册中心,其他服务可以通过监听注册中心来动态发现和获取所需服务实例。在正常的生产环境中,我们不可能对每个接口进行单独的路由配置,所以我们可以通过服务注册与发现来动态生成路由。
我们只需要简单配置有关配置中心连接的配置和Spring Cloud Gateway的路由自动生成配置即可:
spring:
cloud:
gateway:
discovery:
locator:
# 根据注册中心的服务自动生成路由
enabled: true
# 路由名转小写
lower-case-service-id: true
nacos:
discovery:
watch:
enabled: true
server-addr: nacos-server:18848 # nacos访问ip
namespace: test
group: NOOP_GROUP # 分组名
username: nacos # nacos用户名
password: nacos # nacos密码
这里我实现了spring-cloud-starter-alibaba-nacos-discovery这个依赖中的Subscriber<InstancesChangeEvent>来实现对注册中心的动态监听,产生事件时,提醒服务做出一些处理(后续可以用于预处理swagger在线接口文档,并根据服务种类数量动态更新在线文档)

上面我们已经提到了两种路由配置方式,接下来我们详细聊聊路由配置的路由断言工厂 - Predicate Factory
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件。
例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的。
像这样的断言工厂在SpringCloudGateway还有十几个:
| 名称 | 说明 | 示例 |
|---|---|---|
| After | 是某个时间后的请求 | - After=2024-06-02T11:55:58.789-07:00[America/Denver] |
| Before | 是某个时间点之前的请求 | - Before=2024-06-02T14:35:230.433+08:00[Asia/Shanghai] |
| Between | 是某两个时间点之间的请求 | - Between=2024-06-02T11:55:58.789-07:00[America/Denver], 2024-06-03T11:55:58.789-07:00[America/Denver] |
| Cookie | 请求必须包含某些Cookie | - Cookie=chocolate,ch.p |
| Header | 请求必须包含某些Header | - Header=X-Request-Id, \d+ |
| Host | 请求必须是访问某个host(域名) | - Host=**.somehost.org,**.anotherhost.org |
| Method | 请求方式必须是指定方法 | - Method=GET,POST |
| Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
| Query | 请求路径必须包含指定参数 | - Query=name,Jack |
| RemoteAddr | 请求者的ip必须是指定范围 | - RemoteQAddr=192.168.1.1/24 |
| Weight | 权重处理 |
在上表中,有很多类型的Predicate。
比如说时间类型的Predicated(AfterRoutePredicateFactory、 BeforeRoutePredicateFactory、BetweenRoutePredicateFactory),当只有满足特定时间要求的请求才会交由router处理;
除了这种方法,我们还可以将重写的RouteLocator注入Spring Bean,达到使用代码手动控制路由的实现。下面是一个简单的例子:
@SpringBootApplication
public class DemogatewayApplication {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://httpbin.org"))
.route("host_route", r -> r.host("*.myhost.org")
.uri("http://httpbin.org"))
.route("rewrite_route", r -> r.host("*.rewrite.org")
.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
.uri("http://httpbin.org"))
.route("hystrix_route", r -> r.host("*.hystrix.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
.uri("http://httpbin.org"))
.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
.uri("http://httpbin.org"))
.route("limit_route", r -> r
.host("*.limited.org").and().path("/anything/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
.uri("http://httpbin.org"))
.build();
}
}
我们同样有两种方式可以编写过滤器,一种是在配置中编写对应路由的过滤器规则,另一种是在代码中自定义全局过滤器(GlobalFilter)。
首先介绍过滤器规则:
| 过滤规则 | 实例 | 说明 |
|---|---|---|
| PrefixPath | - PrefixPath=/app | 在请求路径前加上app |
| RewritePath | - RewritePath=/test, /app/test | 访问localhost:9022/test,请求会转发到localhost:8001/app/test |
| SetPath | SetPath=/app/ | 通过模板设置路径,转发的规则时会在路径前增加app,{path}表示原请求路径 |
| RedirectTo | 重定向 | |
| RemoveRequestHeader | 去掉某个请求头信息 |
注:当配置多个filter时,优先定义的会被调用,剩余的filter将不会生效。
下面是一个简单的例子,是改写路径过滤规则的实,/where/... 改成 test/...:
spring:
cloud:
gateway:
routes:
- id: rewrite_filter
uri: http://localhost:8081
predicates:
- Path=/test/**
filters:
- RewritePath=/where(?<segment>/?.*), /test(?<segment>/?.*)
我们以cors跨域配置为例,来讲讲这个全局过滤器配置。正常我们在Spring Cloud Gateway中配置Cors配置,需要在yml配置文件中配置以下内容:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
add-to-simple-url-handler-mapping: true
这样Spring就会自动给接受到的请求加上配置中跨域相关的请求头,但是我们在实际使用过程中,我们发现一些请求头会重复,如图,Vary 和 Access-Control-Allow-Origin 两个头重复了两次,其中浏览器对后者有唯一性限制!

所以,我们编写了一个全局过滤器GlobalFilter来进行对Cors请求头的去重。我们以此为例讲解全局过滤器。
要编写自己的全局过滤器,我们需要继承GlobalFilter和Ordered,来实现过滤器的方法和对过滤器的优先级进行定义。
下面是一个简单的例子,看代码应该挺好理解的:
@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
private static final String ANY = "*";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
exchange.getResponse().getHeaders().entrySet().stream()
.filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
.filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)
|| kv.getKey().equals(HttpHeaders.VARY)))
.forEach(kv ->
{
// Vary只需要去重即可
if(kv.getKey().equals(HttpHeaders.VARY)) {
kv.setValue(kv.getValue().stream().distinct().collect(Collectors.toList()));
} else{
List<String> value = new ArrayList<>();
if(kv.getValue().contains(ANY)){ //如果包含*,则取*
value.add(ANY);
kv.setValue(value);
}else{
value.add(kv.getValue().get(0)); // 否则默认取第一个
kv.setValue(value);
}
}
});
}));
}
@Override
public int getOrder() {
// 指定此过滤器位于NettyWriteResponseFilter之后
// 即待处理完响应体后接着处理响应头
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
}
项目中我们也同样将Sentinel集成在了网关模块,对请求进行限流。这是一个简单的配置
@Component
public class GatewayConfig {
/**
* spring 提供的视图解析器
*/
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 注册服务熔断过滤器
* @return GlobalFilter 全局过滤器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* 初始化网关规则
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
// rules.add(
// // resource与微服务应用中的${spring.application.name}保持一致
// new GatewayFlowRule("praise-module").setCount(5).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
/**
* 注册限流异常处理器
* @return SentinelGatewayBlockExceptionHandler 限流异常处理器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCodecConfigurer);
}
/**
* 自定义限流异常处理器
*/
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange,
Throwable throwable) {
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject("此接口被限流了"));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
跨域配置问题:
application.yml中添加有关Cors的请求头配置,但是出现了返回头重复的问题,通过编写全局过滤器删除多余请求头解决了这个问题网关鉴权问题
描述
解决方法
@Bean
@Order(-101)
public GlobalFilter jwtAuthGlobalFilter() {
return (exchange, chain) -> {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpResponse serverHttpResponse = exchange.getResponse();
ServerHttpRequest.Builder mutate = serverHttpRequest.mutate();
if (!authEnabled) {
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
// 请求路径
String requestUrl = serverHttpRequest.getURI().getPath();
// 请求方法
String method = serverHttpRequest.getMethod().name();
// RestFul风格的请求路径
String restFulPath = method + ":" + requestUrl;
// url匹配工具
AntPathMatcher antPathMatcher = new AntPathMatcher();
//对白名单中的地址放行
List<String> whiteList = whiteListProperties.getWhites();
for(String str : whiteList){
if(requestUrl.contains(str)){
log.info("白名单,放行:{}", restFulPath);
return chain.filter(exchange);
}
}
// 从 HTTP 请求头中获取 JWT 令牌
String token = getToken(serverHttpRequest);
if (!StringUtils.hasLength(token)) {
return this.unauthorizedResponse(exchange, serverHttpResponse, MsgCodeUtil.MSG_CODE_JWT_TOKEN_ISNULL);
}
// 对Token解签名,并验证Token是否过期
boolean isJwtNotValid = jwtTokenUtil.isTokenExpired(token);
if(isJwtNotValid){
return this.unauthorizedResponse(exchange, serverHttpResponse, MsgCodeUtil.MSG_CODE_JWT_TOKEN_EXPIRED);
}
// 验证 token 里面的 userId 是否为空
String userId = jwtTokenUtil.getUserIdFromToken(token);
String username = jwtTokenUtil.getUserNameFromToken(token);
String role = jwtTokenUtil.getUserRoleFromToken(token);
if (!StringUtils.hasLength(userId)) {
return this.unauthorizedResponse(exchange, serverHttpResponse, MsgCodeUtil.MSG_CODE_JWT_ILLEGAL_ARGUMENT);
}
// 权限校验
if (rbacEnabled) {
// 超级管理员直接放行
if (AuthConstants.ROOT_ROLE.equals(role)) {
// 设置用户信息到请求
addHeader(mutate, USER_ID, userId);
addHeader(mutate, USER_NAME, username);
// 内部请求来源参数清除
removeHeader(mutate, FROM_SOURCE);
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
// 从redis中读入所有uri->角色对应关系
Map<Object, Object> entries = redisUtil.hmget(AuthConstants.AUTH_CACHE_KEY);
// 角色集合
List<String> authorities = new ArrayList<>();
entries.forEach((path, roles) -> {
// 路径匹配则添加到角色集合中
if (antPathMatcher.match((String) path, restFulPath)) {
authorities.addAll((List<String>) roles);
}
});
if (CollectionUtils.isEmpty(authorities) || !authorities.contains(role)) {
return this.unauthorizedResponse(exchange, serverHttpResponse, MsgCodeUtil.MSG_CODE_JWT_PERMISSION_LIMITED);
}
}
// 设置用户信息到请求
addHeader(mutate, USER_ID, userId);
addHeader(mutate, USER_NAME, username);
// 内部请求来源参数清除
removeHeader(mutate, FROM_SOURCE);
return chain.filter(exchange.mutate().request(mutate.build()).build());
};
}
这算是第一次独立搭建这样一个比较大型的系统吧,之前只是止于学习,没有实际上手操作。通过这次实践,踩了不少坑,也对分布式、SpringCloud、网关等有了更深入的了解,也学习了一些底层源码的优秀之处。一个好的架构,对于业务的编写也是能起到很关键的作用的。
下面这是我们后端项目的完整项目架构,整体都是依托于我们最初的微服务网关模块进行的,网关模块的抗压能力,以及健壮性都是在开发初期需要十分注意的。
