【10】微服务组件网关Gateway

1、 网关简介

1.1 背景分析

多服务调用

  • 当一个大型系统在设计时,经常会被拆分为很多个微服务。那么作为客户端应该如何去调用这许多的微服务。客户端可以直接向微服务发送请求,每个微服务都只有一个公开的请求地址,该url地址可以直接映射到具体的微服务,如果没有网关的存在,我们只能在客户端,记录每个微服务的地址,然后分别去调用。
  • 这样的框架会存在诸多的问题,例如:
    • 每个业务都会需要鉴权、限流、权限校验、跨域等逻辑,如果每个业务都各自为战,自己造轮子实现一遍,效率会十分低下,这些步骤完全可以抽取出来,在一个地方统一去做一次就行。
    • 随着业务越来越复杂,比如淘宝打开一个页面可能会涉及到数百个微服务协同工作,如果每一个微服务都分配一个域名的话,一方面客户端代码会很难维护,涉及到数百个域名,另一方面是连接数的瓶颈,这在移动端下会显得非常低效。
  • 基于微服务架构中的设计及实现上的问题,为了在项目中简化前端的调用逻辑,同时也简化内部服务之间相互调用的复杂度,更好的保护内部服务,提出了网关的概念。
    网关的作用

1.2 网关概述

  • 网关本质上提供了一个各种服务访问的入口,并提供服务去接收或者转发所有内部和外部客户端的调用,还有就是权限认证,限流控制等等。

2、Spring Cloud Gateway

  • Spring Cloud Gateway是Spring公司基于Spring5.0、Spring Boot 2.0 等技术开发的一个网关组件,它旨在为微服务架构提供一种有效和统一的API入口,负责服务请求路由、组合及协议转换,并且基于Filter链的方式提供了权限认证,监控,限流等功能。
  • 它是Spring Cloud官方推出的第二代网关框架,定位于取代 Netflix Zuul1.0。相比 Zuul 来说,Spring Cloud Gateway 提供更优秀的性能,更强大的功能。
  • 它是由 WebFlux + Netty + Reactor 实现的响应式的 API 网关。它不能在传统的 servlet 容器中工作,也不能构建成 war 包

2.1 功能特性

  • 性能强劲:是第一代网关Zuul的1.6倍。
  • 功能强大:内置了很多实用的功能,例如:转发、监控、限流等。
  • 动态路由:能够匹配任何请求属性,并且支持路径重写;
  • 集成 Spring Cloud 服务注册发现功能(Nacos、Eruka);
  • 可集成流控降级功能(Sentinel、Hystrix);
  • 可以对路由指定易于编写的 Predicate(断言)和 Filter(过滤器);

2.2 核心概念

  • 路由Route):路由是网关中最基础的部分,路由信息包括一个路由id、一个目标url、一组断言工厂、一组过滤器组成。如果断言为真,则请求的URL和配置的路由成功匹配
  • 断言Predicate):Java8中的断言函数,SpringCloud Gateway中的断言函数类型是Spring5.0框架中的ServerWebExchange。断言函数允许开发者去定义匹配Http request中的任何信息,比如请求头和参数等。
  • 过滤器Filter):SpringCloud Gateway中的filter分为Gateway FilIer和Global Filter。Filter可以对请求和响应进行处理

2.3 工作原理

Gateway工作原理

  • Gateway Client向Gateway Server发送请求;
  • 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文;
  • 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping;
  • RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用;
  • 如果断言成功,由FilteringWebHandler创建过滤器链并调用;
  • 请求会经过PreFilter–微服务–PostFilter的方法,最终返回响应;

3、 快速开始

  • 通过网关作为服务的访问入口,来对系统中的服务进行访问。
  • 例如,通过网关服务去访问sca-providers服务。

3.1 导入依赖

  • 第一步:创建sca-gateway模块,在pom文件中引入gateway和nacos注册中心的依赖。

    • 注意:可能会和spring-webmvc的依赖冲突,需要排除spring-webmvc
            <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>
    

3.2 编写配置文件

  • 第二步:编写yml配置文件
    server:
      port: 9000
    spring:
      application:
        name: sca-gateway
      cloud:
      	nacos:
      	  discovery:
      	    server-addr: 127.0.0.1:8848
        gateway:
          routes: #配置网关路由规则
            - id: route01 #路由id,全局唯一值,用来标识不同的路由信息
              uri: http://localhost:8081/ # 客户端请求需要被转发到某个服务的地址
              predicates: # 配置断言信息(就是路由转发时需要满足的条件)
                - Path=/nacos/provider/echo/**   #设置路由转发的具体路径
              filters:  # 网关过滤器,用于修改请求和响应信息
                - StripPrefix=1 # 转发之前去掉path中第一层路径,如nacos
    

3.3 测试

  • 第三步:创建启动类
    @SpringBootApplication
    public class GatewayApplication {
        public static void main(String[] args) {
            SpringApplication.run(GatewayApplication.class, args);
        }
    }
    
  • 第四步:启动项目进行测试。
    • 依次启动sca-providers,sca-gateway服务,然后打开浏览器进行测试。

4、负载均衡设计

4.1 为什么需要负载均衡

  • 网关才是服务访问入口,所有服务都会在网关层面进行底层映射,所以,在访问服务时,要基于服务service id(服务名)去查找对应的服务,让请求从网关层进行均衡转发,以此来平衡服务实例的处理能力。

4.2 Gateway中负载均衡的实现

  • 第一步:在项目中添加服务发现依赖。
    	<dependency>
             <groupId>com.alibaba.cloud</groupId>
             <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    
  • 第二步:修改其配置文件。
    server:
      port: 9000
    spring:
      application:
        name: sca-gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        gateway:
          discovery:
            locator:
              enabled: true #开启通过服务名查找服务实例的特性,就可以按照网关地址/微服务名称/具体接口路径 的格式去访问
          routes: #配置网关路由规则
            - id: route01 #路由id,可自己指定一个唯一值
              uri: lb://sca-providers # lb指从nacos中按照名称获取微服务,并遵循负载均衡策略
              predicates: #匹配请求规则
                - Path=/nacos/provider/echo/**   #请求路径定义
              filters:  #网关过滤器,用于对请求规则中的内容进行判断分析以及处理
                - StripPrefix=1 #转发之前去掉path中第一层路径,例如nacos
    
  • 建议开发阶段,打开gateway日志,代码如下:
    logging:
      level:
        org.springframework.cloud.gateway: debug
    

5、 Predicate 断言工厂

  • 当请求gateway的时候, 使用断言对请求进行匹配, 如果匹配成功就路由转发, 如果匹配失败就返回404;
  • 断言其本质就是定义路由转发的条件。
  • 官网参考

5.1 内置断言工厂

  • SpringCloud Gateway提供了一些内置的断言工厂,所有这些断言都与HTTP请求的不同属性进行匹配;
  • 基于Datetime类型的断言工厂
    • 此类型的断言根据时间判断,主要有三个:
      • AfterRoutePredicateFactory:判断请求日期是否晚于指定日期。
      • BeforeRoutePredicateFactory:判断请求日期是否早于指定日期。
      • BetweenRoutePredicateFactory:判断请求日期是否在指定时间段内。
      - After=2021-08-02T00:00:00.000+00:00[Asia/Shanghai] # 请求时间在设定时间之后才能访问
      - Before=2021-08-12T00:00:00.000+08:00[Asia/Shanghai] # 请求时间在设定时间之前才能访问
      ``
      
    • 在请求时,如果与配置的时间不吻合,就会返回404 not found。
  • 基于Header的断言工厂
    • HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式;
    • 判断请求头Header是否具有给定名称,且值与正则表达式匹配。
       - Header=X-Request-Id,\d+ #判断请求头是否具有给定名称且值与正则表达式匹配
      
  • 基于Method请求方法的断言工厂
    • MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
      - Method=GET  #设定请求方式为GET
      
  • 基于Query请求参数的断言工厂
    • QueryRoutePredicateFactory:接收两个参数,请求参数和正则表达式;
    • 判断请求参数是否具有给定名称,且值与正则表达式匹配。
      - Query=pagesize,10 #接收请求参数param和给定值,判断是否匹配
      
  • 基于远程地址的断言工厂
    • RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中;
      - RemoteAddr=192.168.1.1/24
      
  • 基于Cookie的断言工厂
    • CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。
    • 判断请求cookie是否具有给定名称,且值与正则表达式匹配。
      -Cookie=chocolate, ch.
      
  • 基于Host的断言工厂
    • HostRoutePredicateFactory:接收一个参数,判断请求的Host是否满足匹配规则
      -Host=**.testhost.org
      
  • 基于Path请求路径的断言工厂
    • PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
      -Path=/foo/{segment}
      
  • 基于路由权重的断言工厂
    • WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
      routes:
          -id: weight_route1 
           uri: host1 
           predicates:
              -Path=/product/**
              -Weight=group3, 1
          -id: weight_route2 
          uri: host2 
          predicates:
              -Path=/product/**
              -Weight= group3, 9
      

5.2 Predicate应用案例

  • 配置ymal文件
    server:
      port: 9000
    spring:
      application:
        name: sca-gateway
      cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848
        gateway:
          discovery:
            locator:
              enabled: true
          routes:
            - id: route01
              uri: lb://sca-providers
              predicates:
                - Path=/nacos/provider/echo/**   #请求路径定义
                - After=2021-08-02T00:00:00.000+00:00[Asia/Shanghai] 
                - Before=2021-08-12T00:00:00.000+08:00[Asia/Shanghai] 
                - Header=X-Request-Id,\d+ #判断请求头是否具有给定名称且值与正则表达式匹配
                - Method=GET  #设定请求方式为GET
                - Query=pagesize,10 #接收请求参数param和给定值,判断是否匹配
              filters:  
                - StripPrefix=1 
    

5.3 自定义路由断言工厂

  • 自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法。
  • 在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
  • 必要条件:
    • 必须为Spring的组件Bean;
    • 类名必须加上RoutePredicateFactory作为结尾;
    • 类必须继承AbstractRoutePredicateFactory
    • 类中必须声明一个静态内部类 ,并定义相关属性来接收配置文件中对应的断言信息;
    • 需要结合shortcutFieldOrder进行绑定;
    • 通过apply进行逻辑判断 true就是匹配成功 false匹配失败;
  • 第一步:创建自定义断言工厂类:
    @Component
    @Slf4j
    //注意:类名为 xxxxRoutePredicateFactory
    public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {
    
        public CheckAuthRoutePredicateFactory() {
            super(Config.class);
        }
    
    	//声明静态内部类,接收断言信息
    	public static class Config {
            private String name;
            public String getName() {
                return name;
            }
            public void setName(String name) {
                this.name = name;
            }
        }
    	
    	 @Override
        public List<String> shortcutFieldOrder() {
            return Collections.singletonList("name");
        }
    
        @Override
        public Predicate<ServerWebExchange> apply(Config config) {
            return new GatewayPredicate() {
                @Override
                public boolean test(ServerWebExchange serverWebExchange) {
                    if(config.getName().equals("admin")){
                        return true;
                    }
                    return false;
                }
            };
        }
    }
    
  • 第二步:配置yml文件
    spring:
      cloud:
        gateway:
          routes:
          - id: order_route  #路由ID,全局唯一
            uri: http://localhost:8020  #目标微服务的请求地址和端口
            predicates:
             # 测试:http://localhost:8888/order/findOrderByUserId/1
            - Path=/order/**   #Path路径匹配
            # 自定义CheckAuth断言工厂 
            - CheckAuth=admin
    

6、GatewayFilter 过滤器工厂

  • 过滤器(Filter)就是在请求传递过程中,对请求和响应做一个处理。
  • Gateway 的Filter从作用范围可分为两种:GatewayFilterGlobalFilter
    • (1)GatewayFilter:应用于单个路由或者一个分组的路由上,称为 局部过滤器;
    • (2)GlobalFilter:应用到所有的路由上,称为 全局过滤器;

6.1 局部过滤器

  • 在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器(GatewayFilter),可以进行一些业务逻辑处理器,比如添加剔除响应头,添加/去除参数等;
  • 官网参考
  • AddRequestHeader
    • 作用:为原始请求添加Header
    • 参数:Header的名称及值
    • 例如:为原始请求添加一个名称为:X-Request-Foo,值为:Bar的请求头。
    spring:
      cloud:
        gateway:
          routes:
            - id: add_request_header_route
              uri: https://example.org
              filters:
                - AddRequestHeader=X-Request-Foo, Hello
    
  • AddRequestParameter:
    • 作用:为原始请求添加请求参数及值。
    • 参数:参数名称及值。
    • 例如:为原始请求添加一个名称为:foo,值为:bar的参数。
    spring:
      cloud:
        gateway:
          routes:
            - id: add_request_parameter_route
              uri: https://example.org
              filters:
                - AddRequestParameter=foo, bar
    
  • PrefixPath:
    • 作用:为原始的请求路径添加统一前缀路径
    • 参数:前缀路径
    • 例如:访问${GATEWAY_URL}/hello 会转发到uri:mypath/hello。
    spring:
      cloud:
        gateway:
          routes:
            - id: prefixpath_route
              uri: https://example.org
              filters:
                - PrefixPath=/mypath
    
  • RequestSize:
    • 作用:设置允许接收最大请求包的大小
    • 参数:请求包大小,单位为字节,默认值为5M
    • 例如:如果请求包大小超过设置的值,则会返回 413 Payload Too Large以及一个errorMessage。
    spring:
      cloud:
        gateway:
          routes:
            - id: request_size_route
          uri: http://localhost:8080/upload
          predicates:
            - Path=/upload
          filters:
            - name: RequestSize
              args:
                # 单位为字节
                maxSize: 5000000
    
  • RedirectTo:
    • 作用:将原始请求重定向到指定的URL
    • 参数:http状态码及重定向的url
    • 例如:将原始请求重定向到百度
    spring:
      cloud:
        gateway:
          #设置路由:路由id、路由到微服务的uri、断言
          routes:
          - id: order_route  #路由ID,全局唯一
            uri: http://localhost:8020  #目标微服务的请求地址和端口
            #配置过滤器工厂
            filters:
            - RedirectTo=302, https://www.baidu.com/  #重定向到百度
    
  • 自定义过滤器工厂:
    • 类需要继承AbstractNameValueGatewayFilterFactory
    • 类名必须要以GatewayFilterFactory结尾并且交给spring管理;
    @Component
    @Slf4j
    public class CheckAuthGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    
        @Override
        public GatewayFilter apply(NameValueConfig config) {
            return (exchange, chain) -> {
                log.info("调用CheckAuthGatewayFilterFactory==="
                        + config.getName() + ":" + config.getValue());
                return chain.filter(exchange);
            };
        }
    }
    
    • 配置自定义的过滤器工厂:
    spring:
      cloud:
        gateway:
          #设置路由:路由id、路由到微服务的uri、断言
          routes:
          - id: order_route  #路由ID,全局唯一
            uri: http://localhost:8020  #目标微服务的请求地址和端口
            #配置过滤器工厂
            filters:
            - CheckAuth=fox,

6.2 全局过滤器

全局过滤器

  • 全局过滤器(GlobalFilter)作用于所有路由,无需配置。在系统初始化时加载,并作用在每个路由上。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
  • 一般内置的全局过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现。
  • 自定义全局过滤器:
    • 例如,当客户端第一次请求服务时,服务端对用户进行信息认证(登录) ,通过数据库的比对,来验证登录信息是否正确来判断是否过滤信息。
    /**
     *定义一个全局过滤器,模拟统一认证,Spring Cloud Gateway规范中,要求所有的全局过滤器必须实现GlobalFilter接口
     */
    public class AuthGlobalFilter implements GlobalFilter, Ordered {
        /**
         * 对请求进行过滤
         * @param exchange (网关层面的过滤器,通过此对象获取请求,响应对象)
         * @param chain 过滤链(这个过滤链中包含0个或者多个过滤器)
         * @return Mono 代表Spring WebFlux技术中的0个或者一个响应序列
         */
        // http://localhost:9000/nacos/provider/echo/9000?username=admin
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            //1.获取请求对象
            ServerHttpRequest request =  exchange.getRequest();
            ServerHttpResponse response =  exchange.getResponse();
            //2.获取请求参数数据,并进行数据响应
            String username = request.getQueryParams().getFirst("username");
    
            if(!"admin".equals(username)){
                System.out.println("用户名不匹配,认证失败");
                //设置状态码
                response.setStatusCode(HttpStatus.UNAUTHORIZED);//401
                //终止请求继续传递
                return response.setComplete();
            }
            //调用chain.filter继续向下执行
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return -200;//数字越小优先级越高
        }
    }
    
    • 启动Gateway服务,假如在访问中的url不带user="admin”这个参数,就会出现异常。

6.3 开启网关日志及跨域配置

  • 要启用 Reactor Netty 访问日志,请设置
    • -Dreactor.netty.http.server.accessLogEnabled=true
  • Gateway跨域配置(CORS Configuration)
    • 官网参考
    • 通过yaml配置文件:
      spring:
        cloud:
          gateway:
              globalcors:
                cors-configurations:
                  '[/**]':
                    allowedOrigins: "*"
                    allowedMethods:
                    - GET
                    - POST
      
    • 通过Java配置类:
      @Configuration
      public class CorsConfig {
          @Bean
          public CorsWebFilter corsFilter() {
              CorsConfiguration config = new CorsConfiguration();
              config.addAllowedMethod("*");
              config.addAllowedOrigin("*");
              config.addAllowedHeader("*");
      
              UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
              source.registerCorsConfiguration("/**", config);
      
              return new CorsWebFilter(source);
          }
      }
      

7、Gateway整合Sentinel流控降级

  • 网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们可以采用Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。参考网址

7.1 限流快速入门

  • 第一步:添加依赖,在原有GateWay的依赖基础上再添加如下两个依赖。
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
    </dependency>
    
  • 第二步:添加sentinel的连接信息以及路由规则。
     routes: #配置网关路由规则
            - id: route01 #路由id,可自己指定一个唯一值
              uri: lb://sca-providers #lb这里表示负载均衡,sca-providers为注册中心的服务名
              predicates: #匹配请求规则
                - Path=/nacos/provider/echo/**   #请求路径定义
     sentinel:
          transport:
            dashboard: localhost:8180 #Sentinel 控制台地址
            port: 8092 #客户端监控API的端口
          eager: true  #取消Sentinel控制台懒加载,即项目启动即连接
    
  • 第三步:启动网关项目,检测sentinel控制台的网关菜单。
    • 启动时,添加sentinel的JVM参数,通过此菜单可以让网关服务在sentinel控制台显示不一样的菜单,代码如下:
      -Dcsp.sentinel.app.type=1
      

环境变量配置页面

  • Sentinel控制台启动以来,界面如下图所示。
  • 说明:假如没有发现请求链路,API管理,则可以关闭网关项目,关闭sentinel,重启。
  • 第四步:在sentinel面板中设置限流策略。
  • 第五步:通过url进行访问检测是否实现了限流操作

7.2 基于请求属性限流。

  • 定义指定routeId的基于属性的限流策略如图所示:
  • 通过postman进行测试分析:

7.3 自定义API维度限流

  • 自定义API分组,是一种更细粒度的限流规则定义,它允许我们利用sentinel提供的API,将请求路径进行分组,然后在组上设置限流规则。
    • 第一步:新建API分组,如图所示:
    • 第二步:新建分组流控规则,如图所示:
    • 第三步:进行访问测试,如图所示

7.4 自定义流控网关返回值

  • 定义配置类,设计流控返回值,代码如下:
    @Configuration
    public class GatewayConfig {
        public GatewayConfig(){
            GatewayCallbackManager.setBlockHandler(
    new BlockRequestHandler() {
                @Override
                public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                    Map<String,Object> map=new HashMap<>();
                    map.put("state",429);
                    map.put("message","two many request");
                    String jsonStr=JSON.toJSONString(map);
                    return ServerResponse.ok().body(Mono.just(jsonStr),String.class);
                }
            });
        }
    }
    
  • 其中,Mono是一个发出(emit)0-1个元素的Publisher.

8.、总结

8.1 重点分析

  • 网关(Gateway)诞生的背景?(第一:统一微服务访问的入口,第二:对系统服务进行保护,第三进行统一的认证,授权,限流)
  • 网关的选型?(Netifix Zuul,Spring Cloud Gateway,…)
  • Spring Cloud Gateway的入门实现(添加依赖,路由配置,启动类)
  • Spring Cloud Gateway中的负载均衡?(网关服务注册,服务的发现,基于uri:lb://服务id方式访问具体服务实例)
  • Spring Cloud Gateway中的断言配置?(掌握常用几个就可,用时可以通过搜索引擎去查)
  • Spring Cloud Gateway中的过滤器配置?(掌握过滤器中的两大类型-局部和全局)
  • Spring Cloud Gateway中的限流设计?(Sentinel)

8.2 FAQ分析

  • Gateway在互联网架构中的位置?(nginx->gateway–>微服务–>微服务)
  • Gateway底层负载均衡的实现?(Ribbon)
  • Gateway应用过程中设计的主要概念?(路由id,路由uri,断言,过滤器)
  • Gateway中你做过哪些断言配置?(after,header,path,cookie,…)
  • Gateway中你用的过滤器有哪些?(添加前缀,去掉前缀,添加请求头,…,负载均衡,…)
  • 503 异常?(服务不可用,检查你调用的服务是否启动ok,路由uri写的是否正确)
  • 启动时解析.yml配置文件异常(格式没有对齐,单词写错)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值