
SpringCloud 入门(下)
本篇文章继承上文,主要结合实际项目 Elevator-backend
来学习Spring Cloud 的组件。
上文链接:http://zxlmdonnie.cn/archives/springcloudru-men
6. Gateway 服务网关
注意:Spring Cloud Gateway 本质上也是个微服务!
Spring Cloud Gateway 具有三个核心功能:
权限控制:网关作为微服务入口,需要校验用户是否有请求资格,如果没有则进行拦截;
路由与负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
我们的 elevator-gateway
模块目录如下:
大致可分为以下几个模块:
配置文件:
pom.xml.yml
文件等过滤器:
filter
Swagger 核心配置
swagger
与安全框架有关的配置
security
启动类
GateWay
配置文件简介
pom.xml
<!--gateway网关依赖 193行 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
如果是我们自己新建的一个项目,还需要引进注册中心依赖:
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
bootstrap-dev.yml 关于此模块部分核心配置如下:
server:
port: 5251 #7115
spring:
application:
name: elevator-gateway #提交到注册中心的微服务名称
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: localhost:8848
file-extension: yaml
shared-configs[0]:
data-id: elevator-common.yaml
refresh: true
gateway:
# 网关跨域配置start---------------------------------
# 开启网关的跨域功能,具体微服务上的跨域需要进行关闭,否则无效
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" # 跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
# 网关跨域配置end---------------------------------
# 网关
discovery:
locator:
####开启以服务id去注册中心上获取转发地址
enabled: true
###路由策略
routes:
###路由id
- id: auth
#### 基于lb负载均衡形式转发
uri: lb://elevator-auth
###匹配规则
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
###路由id
- id: system
#### 基于lb负载均衡形式转发
uri: lb://elevator-system
###匹配规则
predicates:
- Path=/system/**
filters:
- StripPrefix=1
###路由id
- id: unit
#### 基于lb负载均衡形式转发
uri: lb://elevator-unit
###匹配规则
predicates:
- Path=/unit/**
filters:
- StripPrefix=1
###路由id
- id: order
#### 基于lb负载均衡形式转发
uri: lb://elevator-order
###匹配规则
predicates:
- Path=/order/**
filters:
- StripPrefix=1
###路由id
- id: sign
#### 基于lb负载均衡形式转发
uri: lb://elevator-sign
###匹配规则
predicates:
- Path=/sign/**
filters:
- StripPrefix=1
###路由id
- id: example
#### 基于lb负载均衡形式转发
uri: lb://elevator-example
###匹配规则
predicates:
- Path=/example/**
filters:
- StripPrefix=1
###路由id
- id: management
#### 基于lb负载均衡形式转发
uri: lb://elevator-example-customer
###匹配规则
predicates:
- Path=/customer/**
filters:
- StripPrefix=1
大体分模块如下:跨域配置、网关配置、路由策略配置
gateway:
# 开启网关的跨域功能,具体微服务上的跨域需要进行关闭,否则无效
globalcors:
cors-configurations:
# ...
# 网关
discovery:
locator:
# 开启以服务id去注册中心上获取转发地址
enabled: true
# 路由策略
routes:
# 路由id
- id: auth
# 基于lb负载均衡形式转发
uri: lb://elevator-auth
# 匹配规则
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
#路由id
#...
跨域配置、网关配置很好理解,gateway 的核心其实是在路由策略:
核心配置:路由策略
# 路由策略
routes:
# 当前路由id 自定义 只要唯一即可
- id: unit
# 当前路由的目标地址 http就是固定地址
# uri: http://127.0.0.1:8081
# 当前路由的目标地址 lb就是负载均衡,后面跟服务名称
uri: lb://elevator-unit
# 当前路由断言,也就是判断请求是否符合路由规则的条件
predicates:
# 这个是按照路径匹配,只要以/unit/开头就符合要求
- Path=/unit/**
# 当前路由的过滤器
filters:
# StripPrefix=1 表示去除请求路径的第一个路径片段(即前缀路径)。
- StripPrefix=1
我们将符合Path
规则的一切请求,都代理到 uri
参数指定的地址。
本例中,即为将 /unit/**
开头的请求,代理到 lb://elevator-unit
,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。
现在我们把这两个模块运行起来:
打开本模块的 Swagger-ui:http://localhost:7983/swagger-ui/
随便找到一个接口测试,如:
curl -X GET "http://localhost:7983/unit/dictionary/city?provinceId=0" -H "accept: */*"
由于前缀路径 /unit
在过滤器中被去掉,请求被转发到了:http://elevator-unit/dictionary/city?provinceId=0,而elevator-unit模块在localhost:8080,即转发到:
curl -X GET "http://localhost:8080/dictionary/city?provinceId=0" -H "accept: */*"
均可查看到相同的返回结果:
{
"msg": "查询成功",
"success": true,
"data": [
{
"id": 145,
"code": "371000",
"cityName": "威海市",
"provinceId": 15,
"createTime": null
...
Swagger 配置
GateWay 的 Swagger3.0 相关配置 代码by王梓 留档备用。
@Component
@Primary
@RequiredArgsConstructor
@Slf4j
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
// 获取所有路由的ID
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
// 过滤出配置文件中定义的路由->过滤出Path Route Predicate->根据路径拼接成api-docs路径->生成SwaggerResource
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
route.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("**", "v3/api-docs"))));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
log.info("name:{},location:{}",name,location);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("3.0");
return swaggerResource;
}
}
/**
* 聚合各个服务的swagger接口
*/
@Component
@RequiredArgsConstructor
public class SwaggerResourceHandler implements HandlerFunction<ServerResponse> {
private final SwaggerResourcesProvider swaggerResources;
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
Mono<ServerResponse> responseMono = ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters
.fromValue(swaggerResources.get()));
return responseMono;
}
}
/**
* 权限处理器
*/
@Component
public class SwaggerSecurityHandler implements HandlerFunction<ServerResponse> {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
Mono<ServerResponse> responseMono = ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters
.fromValue(Optional.ofNullable(securityConfiguration)
.orElse(SecurityConfigurationBuilder.builder().build())));
return responseMono;
}
}
/**
* UI处理器
*/
@Component
public class SwaggerUiHandler implements HandlerFunction<ServerResponse> {
@Autowired(required = false)
private UiConfiguration uiConfiguration;
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
Mono<ServerResponse> responseMono = ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters
.fromValue(Optional.ofNullable(uiConfiguration)
.orElse(UiConfigurationBuilder.builder().build())));
return responseMono;
}
}
/**
* Swagger路由
*/
@Configuration
@RequiredArgsConstructor
public class SwaggerRouter {
/**
* 聚合各个服务的swagger接口
*/
private final SwaggerResourceHandler swaggerResourceHandler;
/**
* 权限处理器
*/
private final SwaggerSecurityHandler swaggerSecurityHandler;
/**
* UI处理器
*/
private final SwaggerUiHandler swaggerUiHandler;
@Bean
public RouterFunction<ServerResponse> swaggerRouterFunction() {
return RouterFunctions
.route(RequestPredicates.GET("/swagger-resources/configuration/security").and(RequestPredicates.accept(MediaType.ALL)), swaggerSecurityHandler::handle)
.andRoute(RequestPredicates.GET("/swagger-resources/configuration/ui").and(RequestPredicates.accept(MediaType.ALL)), swaggerUiHandler::handle)
.andRoute(RequestPredicates.GET("/swagger-resources").and(RequestPredicates.accept(MediaType.ALL)), swaggerResourceHandler::handle);
}
}
全局过滤器配置
过滤器分两种:
路由过滤器:在yml文件
routes.filter
中配置,处理逻辑固定全局过滤器:实现
GlobalFilter
接口
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
本项目实现这个接口是作为安全框架的善后。
/**
* 安全拦截全局过滤器,非网关鉴权的逻辑
* 在ResourceServerManager#check鉴权善后(如黑名单拦截)
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class GatewaySecurityFilter implements GlobalFilter, Ordered {
private final RedisTemplate redisTemplate;
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 非JWT放行不做后续解析处理
String token = request.getHeaders().getFirst(SecurityConstant.AUTHORIZATION_KEY);
if (StrUtil.isBlank(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstant.JWT_PREFIX)) {
// 放行
return chain.filter(exchange);
}
// 解析JWT获取jti
token = StrUtil.replaceIgnoreCase(token, SecurityConstant.JWT_PREFIX, Strings.EMPTY);
String payload = StrUtil.toString(JWSObject.parse(token).getPayload());
JSONObject jsonObject = JSONUtil.parseObj(payload);
// 以jti为key判断redis的黑名单列表是否存在,存在则拦截访问
String jti = jsonObject.getStr(SecurityConstant.JWT_JTI);
Boolean isBlack = redisTemplate.hasKey(SecurityConstant.TOKEN_BLACKLIST_PREFIX + jti);
if (isBlack) {
return ResponseUtils.writeErrorInfo(response, ResponseCode.TOKEN_ACCESS_FORBIDDEN);
}
// 以clientId + userId为key判断redis的黑名单列表是否存在,存在则拦截访问
String userId = jsonObject.getStr(SecurityConstant.JWT_ID);
String clientId = jsonObject.getStr(SecurityConstant.CLIENT_ID_KEY);
Boolean isRestricted = redisTemplate.hasKey(SecurityConstant.USERID_BLACKLIST_PREFIX + clientId + RedisKeys.SPLIT + userId);
if (isRestricted) {
return ResponseUtils.writeErrorInfo(response, ResponseCode.USERID_ACCESS_FORBIDDEN);
}
// 存在token且不在黑名单中,request写入JWT的载体信息传递给微服务
request = exchange.getRequest().mutate()
.header(SecurityConstant.JWT_PAYLOAD_KEY, URLEncoder.encode(payload, "UTF-8"))
.build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
我们只需关注所有 return
语句:
放行:
return chain.filter(exchange);
安全框架
ResourceServerConfig
为资源服务器配置。
ResourceServerManager
实现了接口 ReactiveAuthorizationManager<AuthorizationContext>
为网关自定义鉴权管理器。
7. 微服务方面设计:Ha-Pancake
项目架构设计阶段
基本模块框架搭建
common
auth
gateway
:使得每个项目都可以独立运行主方法SpringApplication.run()
创建 git 仓库与 README.md
建立示例项目模块,进行
example
实体相关 CRUD配置网关、远程调用、安全框架(可以进行注册、登录开发)
配置注册中心、负载均衡、配置中心、Swagger-UI
进一步优化:安全框架Auth逻辑、自动化部署、配置中心继续优化 ……
建立模块
创建Maven父工程,配置
pom.xml
的dependencymanagement
。创建Maven子工程
被依赖工程(如
common
xxx-client
模块),配置pom.xml
的相关依赖含启动类工程(如
auth-api
system-api
模块),配置pom.xml
相关依赖,并引入本项目的被依赖工程,最后配置build
属性,在打包时包含依赖并指定启动类。
配置依赖时,可以参考 https://start.aliyun.com/
阿里云官方云原生脚手架,可以复制
pom.xml
进行相关配置注意父子工程的依赖关系、ArtifactId、groupName等字段
配置网关、远程调用、安全框架
关于三者结合,最后要实现的效果如下:
Gateway:进行路由转发 配置过滤器,可以路由转发到Auth、System、Example 模块
Auth:进行权限认证,在这里进行
token
生成操作System:在这里进行用户的CRUD
Example:进行注册登录拿到权限后,方能访问该模块。
参考
黑马程序员 SpringCloud 课程 https://www.bilibili.com/video/BV1kH4y1S7wz
若维电梯项目 elevator-backend https://ruoweiedu.com/
SpringCloud:Gateway之StripPrefix使用 https://blog.csdn.net/yunyala/article/details/133152704
2024春季练手项目哈大饼之Ha-Islet组 https://github.com/Werun-backend/Ha-Islet