Spring Cloud Gateway 2.1.0 中文文档

目录

1.How to Include Spring Cloud Gateway2.Glossary3.How It Works4.Route Predicate Factories5.GatewayFilter Factories6.Global Filters7.TLS/SSL8.Configuration9.Reactor Netty Access Logs10.CORS Configuration11.Actuator API12.Developer Guide

该项目提供了一个建立在Spring Ecosystem之上的API网关,包括:Spring 5,Spring Boot 2和Project Reactor。Spring Cloud Gateway旨在提供一种简单而有效的方式来对API进行路由,并为他们提供切面,例如:安全性,监控/指标 和弹性等。

1. 如何在工程中引用Spring Cloud Gateway

要在项目中引入Spring Cloud Gateway,需要引用 grouporg.springframework.cloud和 artifact id为spring-cloud-starter-gatewaystarter。最新的Spring Cloud Release 构建信息,请参阅Spring Cloud Project page

如果应用了该starter,但由于某种原因不希望启用网关,请进行设置spring.cloud.gateway.enabled=false。

重要

Spring Cloud Gateway依赖Spring Boot和Spring Webflux提供的Netty runtime。它不能在传统的Servlet容器中工作或构建为WAR

2. 词汇表

Route 路由:gateway的基本构建模块。它由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。

Predicate 断言:这是一个Java 8 Function Predicate。输入类型是Spring FrameworkServerWebExchange。这允许开发人员可以匹配来自HTTP请求的任何内容,例如Header或参数。

Filter 过滤器:这些是使用特定工厂构建的Spring FrameworkGatewayFilter实例。所以可以在返回请求之前或之后修改请求和响应的内容。

3. 如何工作的


Spring Cloud Gateway Diagram

客户端向Spring Cloud Gateway发出请求。如果Gateway Handler Mapping确定请求与路由匹配,则将其发送到Gateway Web Handler。此handler通过特定于该请求的过滤器链处理请求。图中filters被虚线划分的原因是filters可以在发送代理请求之前或之后执行逻辑。先执行所有“pre filter”逻辑,然后进行请求代理。在请求代理执行完后,执行“post filter”逻辑。

注意

HTTP和HTTPS URI默认端口设置是80和443。

4. 路由断言Factories

Spring Cloud Gateway将路由作为Spring WebFluxHandlerMapping基础结构的一部分进行匹配。Spring Cloud Gateway包含许多内置的路由断言Factories。这些断言都匹配HTTP请求的不同属性。多个路由断言Factories可以通过and组合使用。

4.1 After 路由断言 Factory

After Route Predicate Factory采用一个参数——日期时间。在该日期时间之后发生的请求都将被匹配。

application.yml

spring:cloud:gateway:routes:-id:after_route        uri:http://example.org        predicates:-After=2017-01-20T17:42:47.789-07:00[America/Denver]

4.2 Before 路由断言 Factory

Before Route Predicate Factory采用一个参数——日期时间。在该日期时间之前发生的请求都将被匹配。

application.yml.

spring:cloud:gateway:routes:-id:before_route        uri:http://example.org        predicates:-Before=2017-01-20T17:42:47.789-07:00[America/Denver]

4.3 Between 路由断言 Factory

Between 路由断言 Factory有两个参数,datetime1和datetime2。在datetime1和datetime2之间的请求将被匹配。datetime2参数的实际时间必须在datetime1之后。

application.yml.

spring:cloud:gateway:routes:-id:between_route        uri:http://example.org        predicates:-Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver]

4.4 Cookie 路由断言 Factory

Cookie 路由断言 Factory有两个参数,cookie名称和正则表达式。请求包含次cookie名称且正则表达式为真的将会被匹配。

application.yml

spring:cloud:gateway:routes:-id:cookie_route        uri:http://example.org        predicates:-Cookie=chocolate,ch.p

4.5 Header  路由断言 Factory

Header 路由断言 Factory有两个参数,header名称和正则表达式。请求包含次header名称且正则表达式为真的将会被匹配。

application.yml.

spring:cloud:gateway:routes:-id:header_route      uri:http://example.org      predicates:-Header=X-Request-Id,\d+

4.6 Host 路由断言 Factory

Host 路由断言 Factory包括一个参数:host name列表。使用Ant路径匹配规则,.作为分隔符。application.yml.

spring:cloud:gateway:routes:-id:host_route        uri:http://example.org        predicates:-Host=**.somehost.org,**.anotherhost.org

4.7 Method 路由断言 Factory

Method 路由断言 Factory只包含一个参数: 需要匹配的HTTP请求方式

application.yml.

spring:cloud:gateway:routes:-id:method_route        uri:http://example.org        predicates:-Method=GET

所有GET请求都将被路由

4.8 Path 路由断言 Factory

Path 路由断言 Factory 有2个参数: 一个SpringPathMatcher表达式列表和可选matchOptionalTrailingSeparator标识 .

application.yml.

spring:cloud:gateway:routes:-id:host_route        uri:http://example.org        predicates:-Path=/foo/{segment},/bar/{segment}

例如:/foo/1or/foo/baror/bar/baz的请求都将被匹配

URI 模板变量 (如上例中的segment) 将以Map的方式保存于ServerWebExchange.getAttributes()key为ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE. 这些值将在GatewayFilter Factories使用

可以使用以下方法来更方便地访问这些变量。

Map<String,String>uriVariables=ServerWebExchangeUtils.getPathPredicateVariables(exchange);String segment=uriVariables.get("segment");

4.9 Query 路由断言 Factory

Query 路由断言 Factory 有2个参数: 必选项param和可选项regexp.

application.yml.

spring:cloud:gateway:routes:-id:query_route        uri:http://example.org        predicates:-Query=baz

则包含了请求参数baz的都将被匹配。

application.yml.

spring:cloud:gateway:routes:-id:query_route        uri:http://example.org        predicates:-Query=foo,ba.

如果请求参数里包含foo参数,并且值匹配为ba.表达式,则将会被路由,如:barandbaz

4.10 RemoteAddr 路由断言 Factory

RemoteAddr 路由断言 Factory的参数为 一个CIDR符号(IPv4或IPv6)字符串的列表,最小值为1,例如192.168.0.1/16(其中192.168.0.1是IP地址并且16是子网掩码)。

application.yml.

spring:cloud:gateway:routes:-id:remoteaddr_route        uri:http://example.org        predicates:-RemoteAddr=192.168.1.1/24

如果请求的remote address 为192.168.1.10则将被路由

4.10.1 修改远程地址的解析方式

默认情况下,RemoteAddr 路由断言 Factory使用传入请求中的remote address。如果Spring Cloud Gateway位于代理层后面,则可能与实际客户端IP地址不匹配。

可以通过设置自定义RemoteAddressResolver来自定义解析远程地址的方式。Spring Cloud Gateway网关附带一个非默认远程地址解析程序,它基于X-Forwarded-For header,XForwardedRemoteAddressResolver.

XForwardedRemoteAddressResolver有两个静态构造函数方法,采用不同的安全方法:

XForwardedRemoteAddressResolver::TrustAll返回一个RemoteAddressResolver,它始终采用X-Forwarded-for头中找到的第一个IP地址。这种方法容易受到欺骗,因为恶意客户端可能会为解析程序接受的“x-forwarded-for”设置初始值。

XForwardedRemoteAddressResolver::MaxTrustedIndex获取一个索引,该索引与在Spring Cloud网关前运行的受信任基础设施数量相关。例如,如果SpringCloudGateway只能通过haproxy访问,则应使用值1。如果在访问Spring Cloud Gateway之前需要两个受信任的基础架构跃点,那么应该使用2。

给定以下的header值:

X-Forwarded-For:0.0.0.1,0.0.0.2,0.0.0.3

下面的` maxTrustedIndex值将生成以下远程地址:


Java 配置方式:

GatewayConfig.java

RemoteAddressResolver resolver=XForwardedRemoteAddressResolver.maxTrustedIndex(1);....route("direct-route",r->r.remoteAddr("10.1.1.1","10.10.1.1/24").uri("https://downstream1").route("proxied-route",r->r.remoteAddr(resolver,"10.10.1.1","10.10.1.1/24").uri("https://downstream2"))

5. GatewayFilter Factories

过滤器允许以某种方式修改传入的HTTP请求或返回的HTTP响应。过滤器的作用域是某些特定路由。Spring Cloud Gateway包括许多内置的 Filter工厂。

注意:有关如何使用以下任何过滤器的更详细示例,请查看unit tests.。

5.1 AddRequestHeader GatewayFilter Factory

采用一对名称和值作为参数application.yml.

spring:cloud:gateway:routes:-id:add_request_header_route        uri:http://example.org        filters:-AddRequestHeader=X-Request-Foo,Bar

对于所有匹配的请求,这将向下游请求的头中添加x-request-foo:barheader

5.2 AddRequestParameter GatewayFilter Factory

采用一对名称和值作为参数application.yml.

spring:cloud:gateway:routes:-id:add_request_parameter_route        uri:http://example.org        filters:-AddRequestParameter=foo,bar

对于所有匹配的请求,这将向下游请求添加foo=bar查询字符串

5.3 AddResponseHeader GatewayFilter Factory

采用一对名称和值作为参数

application.yml.

spring:cloud:gateway:routes:-id:add_request_header_route        uri:http://example.org        filters:-AddResponseHeader=X-Response-Foo,Bar

对于所有匹配的请求,这会将x-response-foo:bar头添加到下游响应的header中

5.4 Hystrix GatewayFilter Factory

Hystrix是Netflix开源的断路器组件。Hystrix GatewayFilter允许你向网关路由引入断路器,保护你的服务不受级联故障的影响,并允许你在下游故障时提供fallback响应。

要在项目中启用Hystrix网关过滤器,需要添加对spring-cloud-starter-netflix-hystrix的依赖Spring Cloud Netflix.

Hystrix GatewayFilter Factory 需要一个name参数,即HystrixCommand的名称。

application.yml.

spring:cloud:gateway:routes:-id:hystrix_route        uri:http://example.org        filters:-Hystrix=myCommandName

这将剩余的过滤器包装在命令名为“myCommandName”的HystrixCommand中。

hystrix过滤器还可以接受可选的fallbackUri参数。目前,仅支持forward:预设的URI,如果调用fallback,则请求将转发到与URI匹配的控制器。

application.yml.

spring:cloud:gateway:routes:-id:hystrix_route        uri:lb://backing-service:8088predicates:-Path=/consumingserviceendpoint        filters:-name:Hystrix          args:name:fallbackcmd            fallbackUri:forward:/incaseoffailureusethis-RewritePath=/consumingserviceendpoint,/backingserviceendpoint

当调用hystrix fallback时,这将转发到/incaseoffailureusethisuri。注意,这个示例还演示了(可选)通过目标URI上的'lb`前缀,使用Spring Cloud Netflix Ribbon 客户端负载均衡

主要场景是使用fallbackUri到网关应用程序中的内部控制器或处理程序。但是,也可以将请求重新路由到外部应用程序中的控制器或处理程序,如:

application.yml.

spring:cloud:gateway:routes:-id:ingredients        uri:lb://ingredients        predicates:-Path=//ingredients/**

        filters:

        - name: Hystrix

          args:

            name: fetchIngredients

            fallbackUri: forward:/fallback

      - id: ingredients-fallback

        uri: http://localhost:9994

        predicates:

        - Path=/fallback

在本例中,gateway应用程序中没有fallback实现,但是另一个应用程序中有一个接口实现,注册为“http://localhost:9994”。

在将请求转发到fallback的情况下,Hystrix Gateway过滤还支持直接抛出Throwable。它被作为ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR属性添加到ServerWebExchange中,可以在处理网关应用程序中的fallback时使用。

对于外部控制器/处理程序方案,可以添加带有异常详细信息的header。可以在FallbackHeaders GatewayFilter Factory section.中找到有关它的更多信息。

hystrix配置参数(如 timeouts)可以使用全局默认值配置,也可以使用Hystrix wiki中所述属性进行配置。

要为上面的示例路由设置5秒超时,将使用以下配置:

application.yml.

hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds:5000

5.5 FallbackHeaders GatewayFilter Factory

FallbackHeaders允许在转发到外部应用程序中的FallbackUri的请求的header中添加Hystrix异常详细信息,如下所示:

application.yml.

spring:cloud:gateway:routes:-id:ingredients        uri:lb://ingredients        predicates:-Path=//ingredients/**

        filters:

        - name: Hystrix

          args:

            name: fetchIngredients

            fallbackUri: forward:/fallback

      - id: ingredients-fallback

        uri: http://localhost:9994

        predicates:

        - Path=/fallback

        filters:

        - name: FallbackHeaders

          args:

            executionExceptionTypeHeaderName: Test-Header

在本例中,在运行HystrixCommand发生执行异常后,请求将被转发到localhost:9994应用程序中的fallback终端或程序。异常类型、消息(如果可用)cause exception类型和消息的头,将由FallbackHeadersfilter添加到该请求中。

通过设置下面列出的参数值及其默认值,可以在配置中覆盖headers的名称:

executionExceptionTypeHeaderName("Execution-Exception-Type")

executionExceptionMessageHeaderName("Execution-Exception-Message")

rootCauseExceptionTypeHeaderName("Root-Cause-Exception-Type")

rootCauseExceptionMessageHeaderName("Root-Cause-Exception-Message")

Hystrix 如何实现的更多细节可以参考Hystrix GatewayFilter Factory section.

5.6 PrefixPath GatewayFilter Factory

PrefixPath GatewayFilter 只有一个prefix参数.

application.yml.

spring:cloud:gateway:routes:-id:prefixpath_route        uri:http://example.org        filters:-PrefixPath=/mypath

这将给所有匹配请求的路径加前缀/mypath。因此,向/hello发送的请求将发送到/mypath/hello。

5.7 PreserveHostHeader GatewayFilter Factory

该filter没有参数。设置了该Filter后,GatewayFilter将不使用由HTTP客户端确定的host header ,而是发送原始host header 。

application.yml.

spring:cloud:gateway:routes:-id:preserve_host_route        uri:http://example.org        filters:-PreserveHostHeader

5.8 RequestRateLimiter GatewayFilter Factory

RequestRateLimiter使用RateLimiter实现是否允许继续执行当前请求。如果不允许继续执行,则返回HTTP 429 - Too Many Requests(默认情况下)。

这个过滤器可以配置一个可选的keyResolver参数和rate limiter参数(见下文)。

keyResolver是KeyResolver接口的实现类.在配置中,按名称使用SpEL引用bean。#{@myKeyResolver}是引用名为'myKeyResolver'的bean的SpEL表达式。

KeyResolver.java.

publicinterfaceKeyResolver{Mono<String>resolve(ServerWebExchange exchange);}

KeyResolver接口允许使用可插拔策略来派生限制请求的key。在未来的里程碑版本中,将有一些KeyResolver实现。

KeyResolver的默认实现是PrincipalNameKeyResolver,它从ServerWebExchange检索Principal并调用Principal.getName()。

默认情况下,如果KeyResolver没有获取到key,请求将被拒绝。此行为可以使用spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key(true or false) 和spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code属性进行调整。

说明 无法通过"shortcut" 配置RequestRateLimiter。以下示例无效

application.properties.

# INVALID SHORTCUT CONFIGURATIONspring.cloud.gateway.routes[0].filters[0]=RequestRateLimiter=2,2,#{@userkeyresolver}

5.8.1RedisRateLimiter

Redis的实现基于Stripe实现。它需要使用spring-boot-starter-data-redis-reactiveSpring Boot starter。

使用的算法是Token Bucket Algorithm.。

redis-rate-limiter.replenishRate是你允许用户每秒执行多少请求,而丢弃任何请求。这是令牌桶的填充速率。

``redis-rate-limiter.burstCapacity`是允许用户在一秒钟内执行的最大请求数。这是令牌桶可以保存的令牌数。将此值设置为零将阻止所有请求。

稳定速率是通过在replenishRate和burstCapacity中设置相同的值来实现的。可通过设置burstCapacity高于replenishRate来允许临时突发流浪。在这种情况下,限流器需要在两次突发之间留出一段时间(根据replenishRate),因为连续两次突发将导致请求丢失 (HTTP 429 - Too Many Requests).。

application.yml.

spring:cloud:gateway:routes:-id:requestratelimiter_route        uri:http://example.org        filters:-name:RequestRateLimiter          args:redis-rate-limiter.replenishRate:10redis-rate-limiter.burstCapacity:20

Config.java.

@BeanKeyResolveruserKeyResolver(){returnexchange->Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));}

这定义了每个用户10个请求的限制。允许20个突发,但下一秒只有10个请求可用。KeyResolver是一个简单的获取user请求参数的工具(注意:不建议用于生产)。

限流器也可以定义为RateLimiter接口的实现 bean。在配置中,按名称使用SpEL引用bean。#{@myRateLimiter}是引用名为'myRateLimiter'的bean的SpEL表达式。

application.yml.

spring:cloud:gateway:routes:-id:requestratelimiter_route        uri:http://example.org        filters:-name:RequestRateLimiter          args:rate-limiter:"#{@myRateLimiter}"key-resolver:"#{@userKeyResolver}"

5.9 RedirectTo GatewayFilter Factory

该过滤器有一个status和一个url参数。status是300类重定向HTTP代码,如301。该URL应为有效的URL,这将是Locationheader的值。

application.yml.

spring:cloud:gateway:routes:-id:prefixpath_route        uri:http://example.org        filters:-RedirectTo=302,http://acme.org

这将发送一个302状态码和一个Location:http://acme.orgheader来执行重定向。

5.10 RemoveNonProxyHeaders GatewayFilter Factory

RemoveNonProxyHeaders GatewayFilter Factory 从转发请求中删除headers。删除的默认头列表来自IETF.

The default removed headers are:

Connection

Keep-Alive

Proxy-Authenticate

Proxy-Authorization

TE

Trailer

Transfer-Encoding

Upgrade 要更改此设置,请将spring.cloud.gateway.filter.remove-non-proxy-headers.headers属性设置为要删除的header名称。

5.11 RemoveRequestHeader GatewayFilter Factory

有一个name参数. 这是要删除的header的名称。

application.yml.

spring:cloud:gateway:routes:-id:removerequestheader_route        uri:http://example.org        filters:-RemoveRequestHeader=X-Request-Foo

这将在X-Request-Fooheader被发送到下游之前删除它。

5.12 RemoveResponseHeader GatewayFilter Factory

有一个name参数. 这是要删除的header的名称。

application.yml.

spring:cloud:gateway:routes:-id:removeresponseheader_route        uri:http://example.org        filters:-RemoveResponseHeader=X-Response-Foo

这将在返回到网关client之前从响应中删除x-response-foo头。

5.13 RewritePath GatewayFilter Factory

包含一个regexp正则表达式参数和一个replacement参数. 通过使用Java正则表达式灵活地重写请求路径。

application.yml.

spring:cloud:gateway:routes:-id:rewritepath_route        uri:http://example.org        predicates:-Path=/foo/**

        filters:

        - RewritePath=/foo/(?<segment>.*), /$\{segment}

对于请求路径/foo/bar,将在发出下游请求之前将路径设置为/bar。注意,由于YAML规范,请使用$\替换$。

5.14 RewriteResponseHeader GatewayFilter Factory

包含name,regexp和replacement参数.。通过使用Java正则表达式灵活地重写响应头的值。

application.yml.

spring:cloud:gateway:routes:-id:rewriteresponseheader_route        uri:http://example.org        filters:-RewriteResponseHeader=X-Response-Foo,,password=[^&]+,password=***

对于一个/42?user=ford&password=omg!what&flag=true的header值,在做下游请求时将被设置为/42?user=ford&password=***&flag=true,由于YAML规范,请使用$\替换$。

5.15 SaveSession GatewayFilter Factory

SaveSession GatewayFilter Factory将调用转发到下游之强制执行WebSession::save操作。这在使用Spring Session之类时特别有用,需要确保会话状态在进行转发调用之前已保存。

application.yml.

spring:cloud:gateway:routes:-id:save_session        uri:http://example.org        predicates:-Path=/foo/**

        filters:

        - SaveSession

如果你希望要将[Spring Security](https://projects.spring.io/SpringSecurity/)与Spring Session集成,并确保安全详细信息已转发到远程的进程,这一点至关重要。

5.16 SecureHeaders GatewayFilter Factory

SecureHeaders GatewayFilter Factory 将许多headers添加到reccomedation处的响应中,从this blog post.

添加以下标题(使用默认值分配):

X-Xss-Protection:1; mode=block

Strict-Transport-Security:max-age=631138519

X-Frame-Options:DENY

X-Content-Type-Options:nosniff

Referrer-Policy:no-referrer

Content-Security-Policy:default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'

X-Download-Options:noopen

X-Permitted-Cross-Domain-Policies:none

要更改默认值,请在spring.cloud.gateway.filter.secure-headers命名空间中设置相应的属性:

Property to change:

xss-protection-header

strict-transport-security

frame-options

content-type-options

referrer-policy

content-security-policy

download-options

permitted-cross-domain-policies

5.17 SetPath GatewayFilter Factory

SetPath GatewayFilter Factory 采用template路径参数。它提供了一种通过允许路径的模板化segments来操作请求路径的简单方法。使用Spring Framework中的URI模板,允许多个匹配segments。

application.yml.

spring:cloud:gateway:routes:-id:setpath_route        uri:http://example.org        predicates:-Path=/foo/{segment}filters:-SetPath=/{segment}

对于一个/foo/bar请求,在做下游请求前,路径将被设置为/bar

5.18 SetResponseHeader GatewayFilter Factory

SetResponseHeader GatewayFilter Factory 包括name和value参数.

application.yml.

spring:cloud:gateway:routes:-id:setresponseheader_route        uri:http://example.org        filters:-SetResponseHeader=X-Response-Foo,Bar

此GatewayFilter使用给定的名称替换所有header,而不是添加。因此,如果下游服务器响应为X-Response-Foo:1234,则会将其替换为X-Response-Foo:Bar,这是网关客户端将接收的内容。

5.19 SetStatus GatewayFilter Factory

SetStatus GatewayFilter Factory 包括唯一的status参数.必须是一个可用的SpringHttpStatus。它可以是整数值404或字符串枚举NOT_FOUND。

application.yml.

spring:cloud:gateway:routes:-id:setstatusstring_route        uri:http://example.org        filters:-SetStatus=BAD_REQUEST-id:setstatusint_route        uri:http://example.org        filters:-SetStatus=401

在这个例子中,HTTP返回码将设置为401.

5.20 StripPrefix GatewayFilter Factory

StripPrefix GatewayFilter Factory 包括一个parts参数。parts参数指示在将请求发送到下游之前,要从请求中去除的路径中的节数。

application.yml.

spring:cloud:gateway:routes:-id:nameRoot        uri:http://nameservice        predicates:-Path=/name/**

        filters:

        - StripPrefix=2

当通过网关发出/name/bar/foo请求时,向nameservice发出的请求将是http://nameservice/foo。

5.21 Retry GatewayFilter Factory

Retry GatewayFilter Factory包括retries,statuses,methods和series参数.

retries: 应尝试的重试次数

statuses: 应该重试的HTTP状态代码,用org.springframework.http.HttpStatus标识

methods: 应该重试的HTTP方法,用org.springframework.http.HttpMethod标识

series: 要重试的一系列状态码,用org.springframework.http.HttpStatus.Series标识

application.yml.

spring: cloud: gateway: routes: - id: retry_test uri:http://localhost:8080/flakeypredicates: - Host=*.retry.com filters: - name: Retry args: retries: 3 statuses: BAD_GATEWAY

注意

retry filter 不支持body请求的重试,如通过body的POST 或 PUT请求

注意 在使用带有前缀为forward:的retry filter时,应仔细编写目标端点,以便在出现错误时不会执行任何可能导致将响应发送到客户端并提交的操作。例如,如果目标端点是带注解的controller,则目标controller方法不应返回带有错误状态代码的ResponseEntity。相反,它应该抛出一个Exception,或者发出一个错误信号,例如通过Mono.error(ex)返回值,重试过滤器可以配置为通过重试来处理。

5.22 RequestSize GatewayFilter Factory

当请求大小大于允许的限制时,RequestSize GatewayFilter Factory可以限制请求不到达下游服务。过滤器以RequestSize作为参数,这是定义请求的允许大小限制(以字节为单位)。

application.yml.

spring:cloud:gateway:routes:-id:request_size_route      uri:http://localhost:8080/upload      predicates:-Path=/upload      filters:-name:RequestSize        args:maxSize:5000000

当请求因大小而被拒绝时, RequestSize GatewayFilter Factory 将响应状态设置为413 Payload Too Large,并带有额外的headererrorMessage。下面是一个errorMessage的例子。

errorMessage:Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB

注意

如果未在路由定义中作为filter参数提供,则默认请求大小将设置为5 MB。

5.23 Modify Request Body GatewayFilter Factory

这个过滤器被定义为beta版本,将来API可能会改变。

此过滤器可用于在请求主体被网关发送到下游之前对其进行修改。

注意

只能使用Java DSL配置此过滤器

@BeanpublicRouteLocatorroutes(RouteLocatorBuilder builder){returnbuilder.routes().route("rewrite_request_obj",r->r.host("*.rewriterequestobj.org").filters(f->f.prefixPath("/httpbin").modifyRequestBody(String.class,Hello.class,MediaType.APPLICATION_JSON_VALUE,(exchange,s)->returnMono.just(newHello(s.toUpperCase())))).uri(uri)).build();}staticclassHello{String message;publicHello(){}publicHello(String message){this.message=message;}publicStringgetMessage(){returnmessage;}publicvoidsetMessage(String message){this.message=message;}}

5.24 Modify Response Body GatewayFilter Factory

这个过滤器被定义为beta版本,将来API可能会改变。

此过滤器可用于在将响应正文发送回客户端之前对其进行修改。

注意

只能使用Java DSL配置此过滤器

@BeanpublicRouteLocatorroutes(RouteLocatorBuilder builder){returnbuilder.routes().route("rewrite_response_upper",r->r.host("*.rewriteresponseupper.org").filters(f->f.prefixPath("/httpbin").modifyResponseBody(String.class,String.class,(exchange,s)->Mono.just(s.toUpperCase()))).uri(uri).build();}

6. Global Filters

GlobalFilter接口与GatewayFilter具有相同的签名。是有条件地应用于所有路由的特殊过滤器。(此接口和用法可能在将来的里程碑版本中发生更改)。

6.1 全局Filter和GatewayFilter组合排序

当请求进入(并与路由匹配)时,筛选Web Handler 会将GlobalFilter的所有实例和所有的GatewayFilter路由特定实例添加到 filter chain。filter组合的排序由org.springframework.core.Ordered接口决定,可以通过实现getOrde()方法或使用@Order注释来设置。

由于Spring Cloud Gateway将用于执行过滤器逻辑区分为“前置”和“后置”阶段,具有最高优先级的过滤器将是“前置”阶段的第一个,而“后置”阶段的最后一个。

ExampleConfiguration.java.

@Bean@Order(-1)publicGlobalFiltera(){return(exchange,chain)->{log.info("first pre filter");returnchain.filter(exchange).then(Mono.fromRunnable(()->{log.info("third post filter");}));};}@Bean@Order(0)publicGlobalFilterb(){return(exchange,chain)->{log.info("second pre filter");returnchain.filter(exchange).then(Mono.fromRunnable(()->{log.info("second post filter");}));};}@Bean@Order(1)publicGlobalFilterc(){return(exchange,chain)->{log.info("third pre filter");returnchain.filter(exchange).then(Mono.fromRunnable(()->{log.info("first post filter");}));};}

6.2 Forward Routing Filter

ForwardRoutingFilter在exchange属性ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR中查找URI。如果URL有一个forwardscheme (如forward:///localendpoint),它将使用SpringDispatcherHandler来处理请求。请求URL的路径部分将被转发URL中的路径覆盖。未修改的原始URL将附加到ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR属性中的列表中。

6.3 LoadBalancerClient Filter

LoadBalancerClientFilter在exchange属性ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR中查找URI。如果URL有一个lbscheme (如lb://myservice),它将使用Spring CloudLoadBalancerClient将名称(在前一个示例中为'myservice)解析为实际主机和端口,并替换URI。未修改的原始URL将附加到ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR属性中的列表中。过滤器还将查看ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR属性,查看它是否等于lb`,然后应用相同的规则。

application.yml.

spring:cloud:gateway:routes:-id:myRoute        uri:lb://service        predicates:-Path=/service/**

注意 默认情况下,如果一个服务实例在LoadBalancer中没有发现,则返回503。可以通过设置spring.cloud.gateway.loadbalancer.use404=true来让网管返回404.

注意

从LoadBalancer返回的ServiceInstance的isSecure值将覆盖在对网关发出的请求中指定的scheme。例如,如果请求通过HTTPS进入网关,但ServiceInstance表示它不安全,则下游请求将通过HTTP协议。相反的情况也适用。但是,如果在网关配置中为路由指定了GATEWAY_SCHEME_PREFIX_ATTR,则前缀将被删除,并且路由URL生成的scheme将覆盖ServiceInstance配置。

6.4 Netty Routing Filter

如果位于ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR属性中的URL具有http或https模式,则会运行Netty Routing Filter。它使用NettyHttpClient发出下游代理请求。响应放在ServerWebExchangeUtils.CLIENT_RESPONSE_ATTRexchange属性中,以便在以后的过滤器中使用。(有一个实验阶段不需要Netty的相同的功能的Filter,WebClientHttpRoutingFilter)

6.5 Netty Write Response Filter

如果ServerWebExchangeUtils.CLIENT_RESPONSE_ATTRexchange属性中存在 NettyHttpClientResponse,则运行NettyWriteResponseFilter。它在其他所有过滤器完成后将代理响应写回网关客户端响应之后运行。(有一个不需要netty的实验性的WebClientWriteResponseFilter执行相同的功能)

6.6 RouteToRequestUrl Filter

如果ServerWebExchangeUtils.GATEWAY_ROUTE_ATTRexchange属性中存在Route对象,RouteToRequestUrlFilter将运行。它基于请求URI创建一个新的URI,使用Route对象的uri属性进行更新。新的URI被放置在ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTRexchange属性中。

如果该URI有一个前缀scheme,例如lb:ws://serviceid,则会从该URI中剥离该lbscheme,并将其放置在ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR中,以便稍后在过滤器链中使用。

6.7 Websocket Routing Filter

如果ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTRexchange属性中有ws、wssscheme,则Websocket Routing Filter将被运行。它使用Spring Web Socket基础模块将Websocket转发到下游。

URI前缀为lb的Websockets可以被负载均衡,如lb:ws://serviceid.

注意 如果使用SockJS作为普通HTTP的fallback,则应配置普通HTTP路由以及WebSocket路由。

application.yml.

spring:cloud:gateway:routes:# SockJS route-id:websocket_sockjs_route        uri:http://localhost:3001predicates:-Path=/websocket/info/**

      # Normwal Websocket route

      - id: websocket_route

        uri: ws://localhost:3001

        predicates:

        - Path=/websocket/**

6.8 Gateway Metrics Filter

要启用网关指标,请将spring-boot-starter-actuator添加为项目依赖项。然后,默认情况下,只要属性spring.cloud.gateway.metrics.enabled未设置为false,网关指标过滤器就会运行。此过滤器添加名为“gateway.requests”的计时器指标,并带有以下标记:

routeId: The route id

routeUri:  API 将被转发的URI

outcome: 结果分类依据HttpStatus.Series

status: 返回client的请求的Http Status

这些指标可以从/actuator/metrics/gateway.requests中获取,可以很容易地与Prometheus集成以创建Grafanadashboard.

注意

要将pometheus启用,需要添加 micrometer-registry-prometheus为项目依赖。

6.9 Making An Exchange As Routed

网关路由ServerWebExchange之后,它将通过向Exchange属性添加gatewayAlreadyRouted,将该exchange标记为“routed”。一旦一个请求被标记为routed,其他路由过滤器将不会再次路由该请求,将跳过该过滤器。有一些方便的方法可以用来将exchange标记为routed,或者检查exchange是否已经routed。

ServerWebExchangeUtils.isAlreadyRouted有一个ServerWebExchange对象并检查它是否已"routed"

ServerWebExchangeUtils.setAlreadyRouted有一个ServerWebExchange对象并将其标记为"routed"

7. TLS / SSL

网关可以通过常规的 Spring server configuration 来侦听HTTPS上的请求。例子:

application.yml.

server:ssl:enabled:truekey-alias:scg    key-store-password:scg1234    key-store:classpath:scg-keystore.p12    key-store-type:PKCS12

网关路由可以路由到HTTP和HTTPS后端。如果路由到HTTPS后端,则可以将网关配置为信任所有具有证书的下游服务:

application.yml.

spring:cloud:gateway:httpclient:ssl:useInsecureTrustManager:true

不建议在生产环境使用不安全的信任管理器。对于生产部署,可以使用一组已知证书配置网关,这些证书可以通过以下方式进行配置:

application.yml.

spring:cloud:gateway:httpclient:ssl:trustedX509Certificates:-cert1.pem-cert2.pem

如果Spring Cloud Gateway未配置受信任证书,则使用默认信任库(可以使用系统属性javax.net.ssl.trustStore覆盖)。

7.1 TLS 握手

网关维护一个用于路由到后端的client池。当通过HTTPS通信时,客户端启动一个TLS握手,其中可能会有很多超时。这些超时可以这样配置(显示默认值):

application.yml.

spring:cloud:gateway:httpclient:ssl:handshake-timeout-millis:10000close-notify-flush-timeout-millis:3000close-notify-read-timeout-millis:0

8. Configuration

Spring Cloud Gateway的配置由RouteDefinitionLocator的集合驱动。

RouteDefinitionLocator.java.

publicinterfaceRouteDefinitionLocator{Flux<RouteDefinition>getRouteDefinitions();}

默认情况下,PropertiesRouteDefinitionLocator使用Spring Boot的@ConfigurationProperties机制加载属性。

以下两个示例是等效的:

application.yml.

spring:cloud:gateway:routes:-id:setstatus_route        uri:http://example.org        filters:-name:SetStatus          args:status:401-id:setstatusshortcut_route        uri:http://example.org        filters:-SetStatus=401

对于网关的大部分用法,配置文件方式是够用的,但一些生产用例更建议从外部源(如数据库)加载配置。未来的里程碑版本将有基于Spring Data Repositories (如Redis、MongoDB和Cassandra)的RouteDefinitionLocator实现。

8.1 Fluent Java Routes API

为了可以更简单在Java中配置,在RouteLocatorBuilderbean中定义了一个fluent API。

GatewaySampleApplication.java.

// static imports from GatewayFilters and RoutePredicates@BeanpublicRouteLocatorcustomRouteLocator(RouteLocatorBuilder builder,ThrottleGatewayFilterFactory throttle){returnbuilder.routes().route(r->r.host("**.abc.org").and().path("/image/png").filters(f->f.addResponseHeader("X-TestHeader","foobar")).uri("http://httpbin.org:80")).route(r->r.path("/image/webp").filters(f->f.addResponseHeader("X-AnotherHeader","baz")).uri("http://httpbin.org:80")).route(r->r.order(-1).host("**.throttle.org").and().path("/get").filters(f->f.filter(throttle.apply(1,1,10,TimeUnit.SECONDS))).uri("http://httpbin.org:80")).build();}

这种样式还允许使用更多的自定义断言。由RouteDefinitionLocatorbeans定义的断言使用逻辑and组合。通过使用fluent Java API,可以在Predicate类上使用and()、or()、negate()运算符。

8.2 DiscoveryClient Route Definition Locator

可以将网关配置为基于使用兼容DiscoveryClient注册中心注册的服务来创建路由。

要启用此功能,请设置spring.cloud.gateway.discovery.locator.enabled=true,并确保DiscoveryClient实现位于classpath上并已启用(如netflix eureka、consul或zookeeper)。

8.2.1 Configuring Predicates and Filters For DiscoveryClient Routes

默认情况下,网关为通过DiscoveryClient创建的路由定义单个断言和过滤器。

默认断言是使用/serviceId/**定义的path断言,其中serviceId是DiscoveryClient中服务的ID。

默认过滤器是使用正则表达式/serviceId/(?<remaining>.*)和替换的/${remaining}进行重写。这只是在请求被发送到下游之前从路径中截取掉 service id 。

可以通过设置spring.cloud.gateway.discovery.locator.predicates[x]andspring.cloud.gateway.discovery.locator.filters[y]来实现自定义DiscoveryClient路由使用的断言and/or过滤器。当你这样做时,如果你想要保留这个功能,你需要确保包括上面的默认断言和过滤器。下面是这样一个例子。

application.properties.

spring.cloud.gateway.discovery.locator.predicates[0].name:Pathspring.cloud.gateway.discovery.locator.predicates[0].args[pattern]:"'/'+serviceId+'/**'"spring.cloud.gateway.discovery.locator.predicates[1].name:Hostspring.cloud.gateway.discovery.locator.predicates[1].args[pattern]:"'**.foo.com'"spring.cloud.gateway.discovery.locator.filters[0].name:Hystrixspring.cloud.gateway.discovery.locator.filters[0].args[name]:serviceIdspring.cloud.gateway.discovery.locator.filters[1].name:RewritePathspring.cloud.gateway.discovery.locator.filters[1].args[regexp]:"'/' + serviceId + '/(?<remaining>.*)'"spring.cloud.gateway.discovery.locator.filters[1].args[replacement]:"'/${remaining}'"

9. Reactor Netty Access Logs

设置-Dreactor.netty.http.server.accessLogEnabled=true来开启Reactor Netty access logs,注意必须是Java System Property而不是Spring Boot property。

logging 模块也可以通过配置单独输出一个access log文件,下面是logback的配置例子:

logback.xml.

<appender name="accessLog"class="ch.qos.logback.core.FileAppender"><file>access_log.log</file><encoder><pattern>%msg%n</pattern></encoder></appender><appender name="async"class="ch.qos.logback.classic.AsyncAppender"><appender-ref ref="accessLog"/></appender><logger name="reactor.netty.http.server.AccessLog"level="INFO"additivity="false"><appender-ref ref="async"/></logger>

10. CORS Configuration

我们可以通过配置网关来控制CORS行为,全局CORS配置是Spring FrameworkCorsConfiguration模式的URL MAP。

application.yml.

spring:cloud:gateway:globalcors:corsConfigurations:'[/**]':allowedOrigins:"http://docs.spring.io"allowedMethods:-GET

例子中将允许从docs.spring.io发出的所有GET请求进行CORS请求。

11. Actuator API

/gateway的actuator端点允许监视Spring Cloud Gateway应用程序并与之交互。要进行远程访问,必须在应用程序属性中暴露HTTP或JMX 端口。

application.properties.

management.endpoint.gateway.enabled=true#defaultvaluemanagement.endpoints.web.exposure.include=gateway

11.1 Retrieving route filters

11.1.1 Global Filters

要检索应用于所有路由的 [global filters],请get请求/actuator/gateway/globalfilters。返回的结果类似于以下内容:

{"org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5":10100,"org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@4f6fd101":10000,"org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@32d22650":-1,"org.springframework.cloud.gateway.filter.ForwardRoutingFilter@106459d9":2147483647,"org.springframework.cloud.gateway.filter.NettyRoutingFilter@1fbd5e0":2147483647,"org.springframework.cloud.gateway.filter.ForwardPathFilter@33a71d23":0,"org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@135064ea":2147483637,"org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@23c05889":2147483646}

返回结果包含已就绪的global filters的详细信息(如org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5)。对于每个global filters,返回结果字符串对应过滤器链中的相应顺序。

11.1.2 Route Filters

要检索应用于路由的 [GatewayFilter factories] ,请get请求/actuator/gateway/routefilters。返回结果类似于以下内容:

{"[AddRequestHeaderGatewayFilterFactory@570ed9c configClass = AbstractNameValueGatewayFilterFactory.NameValueConfig]":null,"[SecureHeadersGatewayFilterFactory@fceab5d configClass = Object]":null,"[SaveSessionGatewayFilterFactory@4449b273 configClass = Object]":null}

返回结果包含应用于所有路由的GatewayFilter的详细信息。显示每个工厂提供字符串格式的相应对象(例如,[SecureHeadersGatewayFilterFactory@fceab5d configClass = Object])。请注意,null值是由于endpoint controller实现不完整造成的,因为它尝试在filter chain中设置对象的顺序,这不适用于GatewayFilter工厂对象。

11.2 Refreshing the route cache

如果要清理路由的缓存,请POST请求/actuator/gateway/refresh。该请求将返回一个没有body的200返回码。

11.3 Retrieving the routes defined in the gateway

要检索网关中定义的路由,发送GET请求/actuator/gateway/routes,返回结果如下所示:

[{"route_id":"first_route","route_object":{"predicate":"org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/1736826640@1e9d7e7d","filters":["OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory$$Lambda$436/674480275@6631ef72, order=0}"]},"order":0},{"route_id":"second_route","route_object":{"predicate":"org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/1736826640@cd8d298","filters":[]},"order":0}]

返回结果中包含网关中所有定义的路由信息,下面表格中描述了返回结果信息:

image.png

11.4 Retrieving information about a particular route

要获取单个路由的信息,发送GET请求/actuator/gateway/routes/{id}(如:/actuator/gateway/routes/first_route),返回结果如下所示:

{"id":"first_route","predicates":[{"name":"Path","args":{"_genkey_0":"/first"}}],"filters":[],"uri":"http://www.uri-destination.org","order":0}]

下面表格中描述了返回结果信息:

image.png

11.5 Creating and deleting a particular route

要创建一个路由,发送POST请求/gateway/routes/{id_route_to_create},参数为JSON结构,具体参数数据结构参考上面章节。

要删除一个路由,发送DELETE请求/gateway/routes/{id_route_to_delete}。

11.6 Recap: list of all endpoints

下表总结了Spring Cloud Gateway actuator endpoints。注意,每个endpoint都是/actuator/gateway作为基本路径。

image.png

12. Developer Guide

TODO: overview of writing custom integrations

12.1 Writing Custom Route Predicate Factories

TODO: document writing Custom Route Predicate Factories

12.2 Writing Custom GatewayFilter Factories

如果要自定义一个GatewayFilter,需要实现GatewayFilterFactory。下面是一个你需要集成的抽象类AbstractGatewayFilterFactory。

PreGatewayFilterFactory.java.

publicclassPreGatewayFilterFactoryextendsAbstractGatewayFilterFactory<PreGatewayFilterFactory.Config>{publicPreGatewayFilterFactory(){super(Config.class);}@OverridepublicGatewayFilterapply(Config config){// grab configuration from Config objectreturn(exchange,chain)->{//If you want to build a "pre" filter you need to manipulate the//request before calling chain.filterServerHttpRequest.Builder builder=exchange.getRequest().mutate();//use builder to manipulate the requestreturnchain.filter(exchange.mutate().request(request).build());};}publicstaticclassConfig{//Put the configuration properties for your filter here}}

PostGatewayFilterFactory.java.

publicclassPostGatewayFilterFactoryextendsAbstractGatewayFilterFactory<PostGatewayFilterFactory.Config>{publicPostGatewayFilterFactory(){super(Config.class);}@OverridepublicGatewayFilterapply(Config config){// grab configuration from Config objectreturn(exchange,chain)->{returnchain.filter(exchange).then(Mono.fromRunnable(()->{ServerHttpResponse response=exchange.getResponse();//Manipulate the response in some way}));};}publicstaticclassConfig{//Put the configuration properties for your filter here}}

12.3 Writing Custom Global Filters

TODO: document writing Custom Global Filters

12.4 Writing Custom Route Locators and Writers

TODO: document writing Custom Route Locators and Writers

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345