spring cloud gateway之filter篇

轉(zhuǎn)載請標(biāo)明出處:
https://www.fangzhipeng.com
本文出自方志朋的博客

在上一篇文章詳細(xì)的介紹了Gateway的Predict涕滋,Predict決定了請求由哪一個路由處理,在路由處理之前趁桃,需要經(jīng)過“pre”類型的過濾器處理措近,處理返回響應(yīng)之后,可以由“post”類型的過濾器處理泽铛。

filter的作用和生命周期

由filter工作流程點(diǎn)裕膀,可以知道filter有著非常重要的作用,在“pre”類型的過濾器可以做參數(shù)校驗(yàn)呻粹、權(quán)限校驗(yàn)壕曼、流量監(jiān)控、日志輸出等浊、協(xié)議轉(zhuǎn)換等腮郊,在“post”類型的過濾器中可以做響應(yīng)內(nèi)容、響應(yīng)頭的修改凿掂,日志的輸出伴榔,流量監(jiān)控等。首先需要弄清一點(diǎn)為什么需要網(wǎng)關(guān)這一層庄萎,這就不得不說下filter的作用了踪少。

作用

當(dāng)我們有很多個服務(wù)時,比如下圖中的user-service糠涛、goods-service援奢、sales-service等服務(wù),客戶端請求各個服務(wù)的Api時忍捡,每個服務(wù)都需要做相同的事情集漾,比如鑒權(quán)、限流砸脊、日志輸出等具篇。

1.png

對于這樣重復(fù)的工作,有沒有辦法做的更好凌埂,答案是肯定的驱显。在微服務(wù)的上一層加一個全局的權(quán)限控制、限流瞳抓、日志輸出的Api Gatewat服務(wù)埃疫,然后再將請求轉(zhuǎn)發(fā)到具體的業(yè)務(wù)服務(wù)層。這個Api Gateway服務(wù)就是起到一個服務(wù)邊界的作用孩哑,外接的請求訪問系統(tǒng)栓霜,必須先通過網(wǎng)關(guān)層。

2.png

生命周期

Spring Cloud Gateway同zuul類似横蜒,有“pre”和“post”兩種方式的filter胳蛮。客戶端的請求先經(jīng)過“pre”類型的filter丛晌,然后將請求轉(zhuǎn)發(fā)到具體的業(yè)務(wù)服務(wù)鹰霍,比如上圖中的user-service,收到業(yè)務(wù)服務(wù)的響應(yīng)之后茵乱,再經(jīng)過“post”類型的filter處理茂洒,最后返回響應(yīng)到客戶端。

3.png

與zuul不同的是瓶竭,filter除了分為“pre”和“post”兩種方式的filter外督勺,在Spring Cloud Gateway中,filter從作用范圍可分為另外兩種斤贰,一種是針對于單個路由的gateway filter智哀,它在配置文件中的寫法同predict類似;另外一種是針對于所有路由的global gateway filer∮校現(xiàn)在從作用范圍劃分的維度來講解這兩種filter瓷叫。

gateway filter

過濾器允許以某種方式修改傳入的HTTP請求或傳出的HTTP響應(yīng)屯吊。過濾器可以限定作用在某些特定請求路徑上。 Spring Cloud Gateway包含許多內(nèi)置的GatewayFilter工廠摹菠。

GatewayFilter工廠同上一篇介紹的Predicate工廠類似盒卸,都是在配置文件application.yml中配置,遵循了約定大于配置的思想次氨,只需要在配置文件配置GatewayFilter Factory的名稱蔽介,而不需要寫全部的類名,比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中寫AddRequestHeader煮寡,而不是全部類名虹蓄。在配置文件中配置的GatewayFilter Factory最終都會相應(yīng)的過濾器工廠類處理。

Spring Cloud Gateway 內(nèi)置的過濾器工廠一覽表如下:

1.png

現(xiàn)在挑幾個常見的過濾器工廠來講解幸撕,每一個過濾器工廠在官方文檔都給出了詳細(xì)的使用案例薇组,如果不清楚的還可以在org.springframework.cloud.gateway.filter.factory看每一個過濾器工廠的源碼。

AddRequestHeader GatewayFilter Factory

創(chuàng)建工程坐儿,引入相關(guān)的依賴,包括spring boot 版本2.0.5体箕,spring Cloud版本Finchley,gateway依賴如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

在工程的配置文件中挑童,加入以下的配置:

server:
  port: 8081
spring:
  profiles:
    active: add_request_header_route

---
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: http://httpbin.org:80/get
        filters:
        - AddRequestHeader=X-Request-Foo, Bar
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  profiles: add_request_header_route

在上述的配置中累铅,工程的啟動端口為8081,配置文件為add_request_header_route站叼,在add_request_header_route配置中娃兽,配置了roter的id為add_request_header_route,路由地址為http://httpbin.org:80/get尽楔,該router有AfterPredictFactory投储,有一個filter為AddRequestHeaderGatewayFilterFactory(約定寫成AddRequestHeader),AddRequestHeader過濾器工廠會在請求頭加上一對請求頭阔馋,名稱為X-Request-Foo玛荞,值為Bar。為了驗(yàn)證AddRequestHeaderGatewayFilterFactory是怎么樣工作的呕寝,查看它的源碼勋眯,AddRequestHeaderGatewayFilterFactory的源碼如下:


public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest().mutate()
                    .header(config.getName(), config.getValue())
                    .build();

            return chain.filter(exchange.mutate().request(request).build());
        };
    }

}

由上面的代碼可知,根據(jù)舊的ServerHttpRequest創(chuàng)建新的 ServerHttpRequest 下梢,在新的ServerHttpRequest加了一個請求頭客蹋,然后創(chuàng)建新的 ServerWebExchange ,提交過濾器鏈繼續(xù)過濾孽江。

啟動工程讶坯,通過curl命令來模擬請求:


curl localhost:8081

最終顯示了從 http://httpbin.org:80/get得到了請求,響應(yīng)如下:

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Connection": "close",
    "Forwarded": "proto=http;host=\"localhost:8081\";for=\"0:0:0:0:0:0:0:1:56248\"",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.58.0",
    "X-Forwarded-Host": "localhost:8081",
    "X-Request-Foo": "Bar"
  },
  "origin": "0:0:0:0:0:0:0:1, 210.22.21.66",
  "url": "http://localhost:8081/get"
}

可以上面的響應(yīng)可知岗屏,確實(shí)在請求頭中加入了X-Request-Foo這樣的一個請求頭辆琅,在配置文件中配置的AddRequestHeader過濾器工廠生效漱办。

跟AddRequestHeader過濾器工廠類似的還有AddResponseHeader過濾器工廠,在此就不再重復(fù)婉烟。

RewritePath GatewayFilter Factory

在Nginx服務(wù)啟中有一個非常強(qiáng)大的功能就是重寫路徑娩井,Spring Cloud Gateway默認(rèn)也提供了這樣的功能,這個功能是Zuul沒有的隅很。在配置文件中加上以下的配置:


spring:
  profiles:
    active: rewritepath_route
---
spring:
  cloud:
    gateway:
      routes:
      - id: rewritepath_route
        uri: https://blog.csdn.net
        predicates:
        - Path=/foo/**
        filters:
        - RewritePath=/foo/(?<segment>.*), /$\{segment}
  profiles: rewritepath_route

上面的配置中撞牢,所有的/foo/*開始的路徑都會命中配置的router率碾,并執(zhí)行過濾器的邏輯叔营,在本案例中配置了RewritePath過濾器工廠,此工廠將/foo/(?<segment>.)重寫為{segment}所宰,然后轉(zhuǎn)發(fā)到https://blog.csdn.net绒尊。比如在網(wǎng)頁上請求localhost:8081/foo/forezp,此時會將請求轉(zhuǎn)發(fā)到https://blog.csdn.net/forezp的頁面仔粥,比如在網(wǎng)頁上請求localhost:8081/foo/forezp/1婴谱,頁面顯示404,就是因?yàn)椴淮嬖?a target="_blank" rel="nofollow">https://blog.csdn.net/forezp/1這個頁面躯泰。

自定義過濾器

Spring Cloud Gateway內(nèi)置了19種強(qiáng)大的過濾器工廠谭羔,能夠滿足很多場景的需求,那么能不能自定義自己的過濾器呢麦向,當(dāng)然是可以的瘟裸。在spring Cloud Gateway中,過濾器需要實(shí)現(xiàn)GatewayFilter和Ordered2個接口诵竭。寫一個RequestTimeFilter话告,代碼如下:


public class RequestTimeFilter implements GatewayFilter, Ordered {

    private static final Log log = LogFactory.getLog(GatewayFilter.class);
    private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
        return chain.filter(exchange).then(
                Mono.fromRunnable(() -> {
                    Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
                    if (startTime != null) {
                        log.info(exchange.getRequest().getURI().getRawPath() + ": " + (System.currentTimeMillis() - startTime) + "ms");
                    }
                })
        );

    }

    @Override
    public int getOrder() {
        return 0;
    }
}

在上面的代碼中,Ordered中的int getOrder()方法是來給過濾器設(shè)定優(yōu)先級別的卵慰,值越大則優(yōu)先級越低沙郭。還有有一個filterI(exchange,chain)方法,在該方法中裳朋,先記錄了請求的開始時間病线,并保存在ServerWebExchange中,此處是一個“pre”類型的過濾器鲤嫡,然后再chain.filter的內(nèi)部類中的run()方法中相當(dāng)于"post"過濾器氧苍,在此處打印了請求所消耗的時間。然后將該過濾器注冊到router中泛范,代碼如下:

    @Bean
    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
        // @formatter:off
        return builder.routes()
                .route(r -> r.path("/customer/**")
                        .filters(f -> f.filter(new RequestTimeFilter())
                                .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
                        .uri("http://httpbin.org:80/get")
                        .order(0)
                        .id("customer_filter_router")
                )
                .build();
        // @formatter:on
    }


重啟程序让虐,通過curl命令模擬請求:

 curl localhost:8081/customer/123

在程序的控制臺輸出一下的請求信息的日志:


2018-11-16 15:02:20.177  INFO 20488 --- [ctor-http-nio-3] o.s.cloud.gateway.filter.GatewayFilter   : /customer/123: 152ms

自定義過濾器工廠

在上面的自定義過濾器中,有沒有辦法自定義過濾器工廠類呢?這樣就可以在配置文件中配置過濾器了“盏矗現(xiàn)在需要實(shí)現(xiàn)一個過濾器工廠赡突,在打印時間的時候对扶,可以設(shè)置參數(shù)來決定是否打印請參數(shù)。查看GatewayFilterFactory的源碼惭缰,可以發(fā)現(xiàn)GatewayFilterfactory的層級如下:

微信截圖_20181204175448.png

過濾器工廠的頂級接口是GatewayFilterFactory浪南,我們可以直接繼承它的兩個抽象類來簡化開發(fā)AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,這兩個抽象類的區(qū)別就是前者接收一個參數(shù)(像StripPrefix和我們創(chuàng)建的這種)漱受,后者接收兩個參數(shù)(像AddResponseHeader)络凿。

過濾器工廠的頂級接口是GatewayFilterFactory,有2個兩個較接近具體實(shí)現(xiàn)的抽象類昂羡,分別為AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory絮记,這2個類前者接收一個參數(shù),比如它的實(shí)現(xiàn)類RedirectToGatewayFilterFactory虐先;后者接收2個參數(shù)怨愤,比如它的實(shí)現(xiàn)類AddRequestHeaderGatewayFilterFactory類。現(xiàn)在需要將請求的日志打印出來蛹批,需要使用一個參數(shù)撰洗,這時可以參照RedirectToGatewayFilterFactory的寫法。


public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {


    private static final Log log = LogFactory.getLog(GatewayFilter.class);
    private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
    private static final String KEY = "withParams";

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(KEY);
    }

    public RequestTimeGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
            return chain.filter(exchange).then(
                    Mono.fromRunnable(() -> {
                        Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
                        if (startTime != null) {
                            StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
                                    .append(": ")
                                    .append(System.currentTimeMillis() - startTime)
                                    .append("ms");
                            if (config.isWithParams()) {
                                sb.append(" params:").append(exchange.getRequest().getQueryParams());
                            }
                            log.info(sb.toString());
                        }
                    })
            );
        };
    }


    public static class Config {

        private boolean withParams;

        public boolean isWithParams() {
            return withParams;
        }

        public void setWithParams(boolean withParams) {
            this.withParams = withParams;
        }

    }
}


在上面的代碼中 apply(Config config)方法內(nèi)創(chuàng)建了一個GatewayFilter的匿名類腐芍,具體的實(shí)現(xiàn)邏輯跟之前一樣差导,只不過加了是否打印請求參數(shù)的邏輯,而這個邏輯的開關(guān)是config.isWithParams()猪勇。靜態(tài)內(nèi)部類類Config就是為了接收那個boolean類型的參數(shù)服務(wù)的设褐,里邊的變量名可以隨意寫,但是要重寫List<String> shortcutFieldOrder()這個方法埠对。
络断。

需要注意的是,在類的構(gòu)造器中一定要調(diào)用下父類的構(gòu)造器把Config類型傳過去项玛,否則會報ClassCastException

最后貌笨,需要在工程的啟動文件Application類中,向Srping Ioc容器注冊RequestTimeGatewayFilterFactory類的Bean襟沮。


    @Bean
    public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() {
        return new RequestTimeGatewayFilterFactory();
    }

然后可以在配置文件中配置如下:

spring:
  profiles:
    active: elapse_route

---
spring:
  cloud:
    gateway:
      routes:
      - id: elapse_route
        uri: http://httpbin.org:80/get
        filters:
        - RequestTime=false
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  profiles: elapse_route

啟動工程锥惋,在瀏覽器上訪問localhost:8081?name=forezp,可以在控制臺上看到开伏,日志輸出了請求消耗的時間和請求參數(shù)膀跌。

global filter

Spring Cloud Gateway根據(jù)作用范圍劃分為GatewayFilter和GlobalFilter,二者區(qū)別如下:

  • GatewayFilter : 需要通過spring.cloud.routes.filters 配置在具體路由下固灵,只作用在當(dāng)前路由上或通過spring.cloud.default-filters配置在全局捅伤,作用在所有路由上

  • GlobalFilter : 全局過濾器,不需要在配置文件中配置巫玻,作用在所有的路由上丛忆,最終通過GatewayFilterAdapter包裝成GatewayFilterChain可識別的過濾器祠汇,它為請求業(yè)務(wù)以及路由的URI轉(zhuǎn)換為真實(shí)業(yè)務(wù)服務(wù)的請求地址的核心過濾器,不需要配置熄诡,系統(tǒng)初始化時加載可很,并作用在每個路由上。

Spring Cloud Gateway框架內(nèi)置的GlobalFilter如下:

image

上圖中每一個GlobalFilter都作用在每一個router上凰浮,能夠滿足大多數(shù)的需求我抠。但是如果遇到業(yè)務(wù)上的定制,可能需要編寫滿足自己需求的GlobalFilter袜茧。在下面的案例中將講述如何編寫自己GlobalFilter菜拓,該GlobalFilter會校驗(yàn)請求中是否包含了請求參數(shù)“token”,如何不包含請求參數(shù)“token”則不轉(zhuǎn)發(fā)路由惫周,否則執(zhí)行正常的邏輯尘惧。代碼如下:


public class TokenFilter implements GlobalFilter, Ordered {

    Logger logger=LoggerFactory.getLogger( TokenFilter.class );
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (token == null || token.isEmpty()) {
            logger.info( "token is empty..." );
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

在上面的TokenFilter需要實(shí)現(xiàn)GlobalFilter和Ordered接口康栈,這和實(shí)現(xiàn)GatewayFilter很類似递递。然后根據(jù)ServerWebExchange獲取ServerHttpRequest,然后根據(jù)ServerHttpRequest中是否含有參數(shù)token啥么,如果沒有則完成請求登舞,終止轉(zhuǎn)發(fā),否則執(zhí)行正常的邏輯悬荣。

然后需要將TokenFilter在工程的啟動類中注入到Spring Ioc容器中菠秒,代碼如下:


@Bean
public TokenFilter tokenFilter(){
        return new TokenFilter();
}

啟動工程,使用curl命令請求:

 curl localhost:8081/customer/123
 

可以看到請沒有被轉(zhuǎn)發(fā)氯迂,請求被終止践叠,并在控制臺打印了如下日志:

2018-11-16 15:30:13.543  INFO 19372 --- [ctor-http-nio-2] gateway.TokenFilter                      : token is empty...

上面的日志顯示了請求進(jìn)入了沒有傳“token”的邏輯。

總結(jié)

本篇文章講述了Spring Cloud Gateway中的過濾器嚼蚀,包括GatewayFilter和GlobalFilter禁灼。從官方文檔的內(nèi)置過濾器講起,然后講解自定義GatewayFilter轿曙、GatewayFilterFactory以及自定義的GlobalFilter弄捕。有很多內(nèi)置的過濾器并沒有講述到,比如限流過濾器导帝,這個我覺得是比較重要和大家關(guān)注的過濾器守谓,將在之后的文章講述。

參考資料

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.1.0.M1/single/spring-cloud-gateway.html

http://www.reibang.com/p/eb3a67291050

https://blog.csdn.net/qq_36236890/article/details/80822051

https://windmt.com/2018/05/08/spring-cloud-14-spring-cloud-gateway-filter

源碼下載

https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-predicate

關(guān)注我:

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末您单,一起剝皮案震驚了整個濱河市斋荞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌虐秦,老刑警劉巖平酿,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讯檐,死亡現(xiàn)場離奇詭異,居然都是意外死亡染服,警方通過查閱死者的電腦和手機(jī)别洪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柳刮,“玉大人挖垛,你說我怎么就攤上這事”牛” “怎么了痢毒?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蚕甥。 經(jīng)常有香客問我哪替,道長,這世上最難降的妖魔是什么菇怀? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任凭舶,我火速辦了婚禮,結(jié)果婚禮上爱沟,老公的妹妹穿的比我還像新娘帅霜。我一直安慰自己,他們只是感情好呼伸,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布身冀。 她就那樣靜靜地躺著,像睡著了一般括享。 火紅的嫁衣襯著肌膚如雪搂根。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天铃辖,我揣著相機(jī)與錄音剩愧,去河邊找鬼。 笑死澳叉,一個胖子當(dāng)著我的面吹牛隙咸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播成洗,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼五督,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瓶殃?” 一聲冷哼從身側(cè)響起充包,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后基矮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淆储,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年家浇,在試婚紗的時候發(fā)現(xiàn)自己被綠了本砰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡钢悲,死狀恐怖点额,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情莺琳,我是刑警寧澤还棱,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站惭等,受9級特大地震影響珍手,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辞做,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一琳要、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凭豪,春花似錦焙蹭、人聲如沸晒杈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拯钻。三九已至帖努,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粪般,已是汗流浹背拼余。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留亩歹,地道東北人匙监。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像小作,于是被迫代替她去往敵國和親亭姥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評論 2 361