本篇文章继承上文,主要结合实际项目 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

项目架构设计阶段

  1. 基本模块框架搭建 common auth gateway :使得每个项目都可以独立运行主方法 SpringApplication.run()

  2. 创建 git 仓库与 README.md

  3. 建立示例项目模块,进行 example 实体相关 CRUD

  4. 配置网关、远程调用、安全框架(可以进行注册、登录开发)

  5. 配置注册中心、负载均衡、配置中心、Swagger-UI

  6. 进一步优化:安全框架Auth逻辑、自动化部署、配置中心继续优化 ……

建立模块

  1. 创建Maven父工程,配置 pom.xmldependencymanagement

  2. 创建Maven子工程

    1. 被依赖工程(如 common xxx-client 模块),配置 pom.xml 的相关依赖

    2. 含启动类工程(如 auth-api system-api 模块),配置 pom.xml 相关依赖,并引入本项目的被依赖工程,最后配置 build 属性,在打包时包含依赖并指定启动类。

配置依赖时,可以参考 https://start.aliyun.com/

阿里云官方云原生脚手架,可以复制 pom.xml 进行相关配置

注意父子工程的依赖关系、ArtifactId、groupName等字段

配置网关、远程调用、安全框架

关于三者结合,最后要实现的效果如下:

  • Gateway:进行路由转发 配置过滤器,可以路由转发到Auth、System、Example 模块

  • Auth:进行权限认证,在这里进行 token 生成操作

  • System:在这里进行用户的CRUD

  • Example:进行注册登录拿到权限后,方能访问该模块。

参考

  1. 黑马程序员 SpringCloud 课程 https://www.bilibili.com/video/BV1kH4y1S7wz

  2. 若维电梯项目 elevator-backend https://ruoweiedu.com/

  3. SpringCloud:Gateway之StripPrefix使用 https://blog.csdn.net/yunyala/article/details/133152704

  4. 2024春季练手项目哈大饼之Ha-Islet组 https://github.com/Werun-backend/Ha-Islet