APISIX 入門(國產微服務網關)

一、 概述

Gitee上的官方文檔鏈接

APISIX 是基于 OpenResty + etcd 實現的云原生高性能可擴展的微服務 API 網關。它是國人開源,目前已經進入 Apache 進行孵化。

  • OpenResty:通過 Lua 擴展 Nginx 實現的可伸縮的 Web 平臺。
  • etcd:Key/Value 存儲系統。

APISIX 通過插件機制,提供了動態負載平衡、身份驗證、限流限速等等功能,當然我們也可以自己開發插件進行拓展

整體架構
  • 動態負載均衡:跨多個上游服務的動態負載均衡,目前已支持 round-robin 輪詢和一致性哈希算法。
  • 身份驗證:支持 key-authJWTbasic-authwolf-rbac 等多種認證方式。
  • 限流限速:可以基于速率請求數并發等維度限制。

并且 APISIX 還支持 A/B 測試、金絲雀發布(灰度發布)、藍綠部署、監控報警、服務可觀測性、服務治理等等高級功能,這在作為微服務 API 網關非常重要的特性。

下面,我們正式進入 APISIX 的極簡入門之旅。

二、安裝

《APISIX 官方文檔 —— 安裝》中,介紹了源碼包、RPM 包、Luarocks、Docker 四種安裝方式。這里我們使用 CentOS 7.X 系統,所以采用 RPM 包。
因為 APISIX 是基于 OpenResty + etcd 來實現,所以需要安裝它們兩個。

2.1 安裝依賴

CentOS 7腳本

# 安裝 epel, `luarocks` 需要它
wget http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo rpm -ivh epel-release-latest-7.noarch.rpm

# 添加 OpenResty 源
sudo yum install yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

# 安裝 OpenResty, etcd 和 編譯工具
sudo yum install -y etcd openresty curl git gcc luarocks lua-devel

# 開啟 etcd server
sudo service etcd start

2.2 安裝 Apache APISIX

通過 RPM 包安裝(CentOS 7),其他安裝方式見鏈接

2.2.1 安裝腳本

sudo yum install -y https://github.com/apache/incubator-apisix/releases/download/1.3/apisix-1.3-0.el7.noarch.rpm
一定要看到最后打印如下信息才安裝成功,否則查看報錯信息來解決。

Installed:
  apisix.noarch 0:1.3-0                                                                                                                                                                                          

Complete!

此時,APISIX 安裝在/usr/local/apisix/目錄,使用如下命令查看各個文件夾:

$ cd /usr/local/apisix/
$ ls -ls
total 40
4 drwxr-xr-x 8 root   root 4096 May  1 11
:40 apisix # APISIX 程序
4 drwx------ 2 nobody root 4096 May  1 20:44 client_body_temp
4 drwxr-xr-x 3 root   root 4096 May  1 20:50 conf # 配置文件
4 drwxr-xr-x 6 root   root 4096 May  1 20:40 dashboard # APISIX 控制臺
4 drwxr-xr-x 5 root   root 4096 May  1 20:40 deps
4 drwx------ 2 nobody root 4096 May  1 20:44 fastcgi_temp
4 drwxrwxr-x 2 root   root 4096 May  1 20:44 logs # 日志文件
4 drwx------ 2 nobody root 4096 May  1 20:44 proxy_temp
4 drwx------ 2 nobody root 4096 May  1 20:44 scgi_temp
4 drwx------ 2 nobody root 4096 May  1 20:44 uwsgi_temp

2.2.2 啟動 APISIX

命令:sudo apisix start
默認情況下,APISIX 啟動在 9080 端口,使用如下命令測試服務是否正常啟動:

$ curl http://127.0.0.1:9080/
{"error_msg":"failed to match any routes"}

三、APISIX 控制臺

APISIX 內置控制臺功能,方便我們進行 APISIX 的 Route、Consumer、Service、SSL、Upstream 的查看與維護。也就是上面看到的dashboard文件夾。
dashboard 缺陷:目前dashboard還存在一些不完善的地方,一些插件的函數配置等不可直接編輯,還是得通過apisix api進行。不過基本的一些配置直接可視化配置、查看很直觀,也夠用。

/usr/local/apisix/conf/nginx.conf配置文件中,設置了 APISIX 控制臺的訪問路徑為/apisix/dashboard。如下文所示:

        location /apisix/dashboard {
                allow 127.0.0.0/24;
                deny all;

            alias dashboard/;

            try_files $uri $uri/index.html /index.html;
        }

考慮到安全性,APISIX 控制臺只允許本機訪問,因此我們需要修改/usr/local/apisix/conf/config.yaml配置文件,增加允許訪問的遠程 IP 地址。如下所示:

  allow_admin:                  # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow
    - 127.0.0.0/24              # If we don't set any IP list, then any IP access is allowed by default.
    - 211.94.246.0/24           # 新增加的遠程IP地址段。

修改完配置后,使用 apisix restart 命令,重啟 APISIX 來生效配置。然后,使用瀏覽器訪問 http://101.133.227.13:9080/apisix/dashboard地址,進入 APISIX 控制臺。結果如下圖所示:

dashboard控制臺頁面

使用默認的“admin/123456”賬號,登錄 APISIX 控制臺。

四、動態負載均衡

對后端服務提供的 API 接口進行負載均衡。我們啟動同一個springboot服務在2個不同端口18080 和 28080

網絡架構

4.1啟動2個微服務命令

java -jar vue-springboot-0.0.1-SNAPSHOT-exec.jar --server.port=18080
java -jar vue-springboot-0.0.1-SNAPSHOT-exec.jar --server.port=28080

4.2 驗證服務是否正常

curl -k --tlsv1 https://localhost:18080/v2/vue/api/programLanguage/getAll
curl -k --tlsv1 https://localhost:28080/v2/vue/api/programLanguage/getAll
返回:
["C","vue","java","PHP","Python","C++"]
如果正常返回數據即可。

4.3 重要概念

一個微服務可以通過 APISIX 的路由、服務、上游和插件等多個實體之間的關系進行配置。 Route(路由)與客戶端請求匹配,并指定它們到達 APISIX 后如何發送到 Upstream(上游,后端 API 服務)。 Service(服務)為上游服務提供了抽象。因此,您可以創建單個 Service 并在多個 Route 中引用它。

4.4 創建 APISIX Upstream(上游,后端 API 服務)

APISIX Upstream,是虛擬主機抽象,對給定的多個服務節點按照配置規則進行負載均衡。它根據配置規則在給定的一組服務節點上執行負載平衡。 因此,單個上游配置可以由提供相同服務的多個服務器組成。每個節點將包括一個 key(地址/ip:port)和一個 value (節點的權重)。 服務可以通過輪詢或一致哈希(cHash)機制進行負載平衡。

配置路由時,可以直接設置 Upstream 信息,也可以使用服務抽象來引用 Upstream 信息。

在 APISIX 控制臺的「Upstream」菜單中,創建一個 APISIX Upstream。如下圖所示:

新建upstream

4.5 創建 APISIX Route

APISIX Route,字面意思就是路由,通過定義一些規則來匹配客戶端的請求,然后根據匹配結果加載并執行相應的 插件,并把請求轉發給到指定 Upstream。


Routes配置

4.6 測試

我們來請求 APISIX 網關地址+url,轉發請求到后端服務。

4.6.1 瀏覽器訪問

地址:http://101.133.227.13:9080/v2/vue/api/programLanguage/getAll,注意:101.133.227.13:9080是APISIX 網關IP和端口號,不是Springboot微服務的。
返回錯誤:

Bad Request
This combination of host and port requires TLS.

默認情況下,Apache APISIX 通過 HTTP 協議代理請求。如果我們的后端托管在 HTTPS 環境中,讓我們使用proxy-rewrite插件將方案更改為 HTTPS,記得點擊頁面下方保存 按鈕才能生效。

proxy-rewrite

4.6.2 瀏覽器再次訪問

http://101.133.227.13:9080/v2/vue/api/programLanguage/getAll
或者命令行方式訪問curl -i -X GET "http://101.133.227.13:9080/v2/vue/api/programLanguage/getAll"
正常返回結果:["C","vue","java","PHP","Python","C++"]

API 也可以通過 HTTPs(9443)端口服務訪問。如果您使用的是自簽名證書,那么通過 curl 命令使用 -k 參數忽略自簽名證書錯誤。注意:使用了https和9443端口。
curl -i -k -X GET "https://101.133.227.13:9443/v2/vue/api/programLanguage/getAll"

https訪問

五、限流限速

APISIX 內置了三個限流限速插件:

  • limit-count:基于“固定窗口”的限速實現。
  • limit-req:基于漏桶原理的請求限速實現。
  • limit-conn:限制并發請求(或并發連接)。

5.1 配置 limit-req 插件

本小節,我們來演示使用 limit-req 插件,畢竟基于漏桶的限流算法,是目前較為常用的限流方式。

漏桶算法(Leaky Bucket)是網絡世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)時經常使用的一種算法,它的主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。
漏桶算法提供了一種機制,通過它,突發流量可以被整形以便為網絡提供一個穩定的流量。

我們已經創建了一個 APISIX Route。這里,我們給該 Route 配置下 limit-req 插件。如下圖所示:
限流插件

5.1.1 屬性說明

  • rate:指定的請求速率(以秒為單位),請求速率超過 rate 但沒有超過 (rate + brust)的請求會被加上延時
  • burst:請求速率超過 (rate + brust)的請求會被直接拒絕
  • rejected_code:當請求超過閾值被拒絕時,返回的 HTTP 狀態碼
  • key:是用來做請求計數的依據,當前接受的 key 有:"remote_addr"(客戶端 IP 地址), "server_addr"(服務端 IP 地址), 請求頭中的"X-Forwarded-For" 或 "X-Real-IP"。

上述截圖配置含義:限制了每秒請求速率為 1大于 1 小于 3的會被加上延時,速率超過 3就會被拒絕。

快速訪問返回包含 503 返回碼的響應頭:

HTTP/1.1 503 Service Temporarily Unavailable
Content-Type: text/html
Content-Length: 194
Connection: keep-alive
Server: APISIX web server

<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>openresty</center>
</body>
</html>

5.2 limit-conn

Apisix 的限制并發請求(或并發連接)插件。

5.2.1屬性:

  • conn: 允許的最大并發請求數。 超過這個比率的請求(低于“ conn” + “ burst”)將被延遲以符合這個閾值。
  • burst: 允許延遲的過多并發請求(或連接)的數量。
  • default_conn_delay: 默認的典型連接(或請求)的處理延遲時間。
  • key: 用戶指定的限制并發級別的關鍵字,可以是客戶端IP或服務端IP。
    例如:可以使用主機名(或服務器區域)作為關鍵字,以便限制每個主機名的并發性。 另外,我們也可以使用客戶端地址作為關鍵字,這樣我們就可以避免單個客戶端用太多的并行連接或請求淹沒我們的服務。
    現在接受以下關鍵字: “remote_addr”(客戶端的 IP),“server_addr”(服務器的 IP),請* 求頭中的“ X-Forwarded-For/X-Real-IP”。
  • rejected_code: 當請求超過閾值時返回的 HTTP狀態碼, 默認值是503。

5.2.2 在 route 頁面中添加 limit-conn 插件

限制并發配置

上面啟用的插件的參數表示只允許一個并發請求。 當收到多個并發請求時,將直接返回 503 拒絕請求。

5.2.3 測試

為了區別limit-req返回的503錯誤,可以返回碼臨時改成504。
訪問不同的url模擬并發:
https://101.133.227.13:9443/v2/vue/api/programLanguage/getAll

https://101.133.227.13:9443/v2/vue/api/programLanguage/getAll?param=value
返回:
504 Gateway Time-out

六、 身份驗證

APISIX 內置了四個身份驗證插件:

  • key-auth:基于 Key Authentication 的用戶認證。
  • JWT-auth:基于 JWT (JSON Web Tokens) Authentication 的用戶認證。
  • basic-auth:基于 basic auth 的用戶認證。
  • wolf-rbac:基于 RBAC 的用戶認證及授權。需要額外搭建 wolf 服務,提供用戶、角色、資源等信息。

本小節,我們來演示使用 JWT-auth 插件。

6.1 配置 JWT-auth 插件

在 APISIX 控制臺的「Consumer」菜單中,創建一個 APISIX Consumer,Consumer 是某類服務的消費者,需與用戶認證體系配合才能使用。添加 JWT Authentication 到一個 service 或 route。 然后 consumer 將其密鑰添加到查詢字符串參數、請求頭或 cookie 中以驗證其請求。

6.1.1 屬性

  • key: 不同的 consumer 對象應有不同的值,它應當是唯一的。不同 consumer 使用了相同的 key ,將會出現請求匹配異常。
  • secret: 可選字段,加密秘鑰。如果您未指定,后臺將會自動幫您生成。
  • algorithm:可選字段,加密算法。目前支持 HS256, HS384, HS512, RS256 和 ES256,如果未指定,則默認使用 HS256。
  • exp: 可選字段,token 的超時時間,以秒為單位的計時。比如有效期是 5 分鐘,那么就應設置為 5 * 60 = 300。

6.1.2 consumer使用 JWT-auth 插件

如下圖所示:
image.png

6.1.3 Route使用 JWT-auth 插件

我們已經創建了一個 APISIX Route。這里,我們給該 Route 配置下 JWT-auth 插件。如下圖所示:


route 配置jwt插件

配置過程很奇怪,官方文檔也沒怎么說明。

6.1.3 測試

調用 jwt-auth 插件提供的簽名接口,獲取 Token。

# key 參數,為我們配置 jwt-auth 插件時,設置的 key 屬性。
curl -i -k -X GET  https://101.133.227.13:9443/apisix/plugin/jwt/sign?key=erbadagang

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJlcmJhZGFnYW5nIiwiZXhwIjoxNTk1ODQxNjM3fQ.TyTsIDMGe8bHeXBtZ_6VjnDabbVGTZ7vHGh-PwLIixFkrXkKWLGvN1v6mlKoNCm_ccfTqU9n8h7QDeWdQZMIsA

6.1.3.1 正常情況

使用 Postman 模擬調用示例 https://101.133.227.13:9443/v2/vue/api/programLanguage/getAll 接口,并附帶上 JWT。如下圖所示:

JWT測試

6.1.3.2 如果缺少token訪問會返回

{"message":"Missing JWT token in request"}

6.1.3.3 token 放到請求參數中

https://101.133.227.13:9443/v2/vue/api/programLanguage/getAll?jwt=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJlcmJhZGFnYW5nIiwiZXhwIjoxNTk1ODQyMTg3fQ.KwOVcSIbo9hOk4Kije8tSdUSZIZ8c-ceViGrZpF6pvhZ7Ky6MZYhsDvoGVWvP9TK0FB-G0TPH_ankkiFFv7htw

6.1.3..4 token過期

{"message":"'exp' claim expired at Mon, 27 Jul 2020 09:20:37 GMT"}

七、配置證書

https://gitee.com/iresty/apisix/blob/master/doc/https-cn.md

八、健康檢查

支持對上游節點的主動和被動健康檢查,在負載均衡時自動過濾掉不健康的節點。
由于控制臺dashboard沒有健康檢查的配置項,所以需要通過APISIX api方式在服務端執行如下語句。
使用 REST Admin API 來控制 Apache APISIX,默認只允許 127.0.0.1 訪問,你可以修改 conf/config.yaml 中的 allow_admin 字段,指定允許調用 Admin API 的 IP 列表。同時需要注意的是,Admin API 使用 key auth 來校驗調用者身份,在部署前需要修改 conf/config.yaml 中的 admin_key 字段,來保證安全。

8.1 Xshell登錄服務器執行

# 配置上游節點負載均衡+健康檢查demo
curl http://127.0.0.1:9080/apisix/admin/routes/016  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uris": ["/*"],
    "plugins": {
        "limit-count": {
            "count": 2,
            "time_window": 60,
            "rejected_code": 503,
            "key": "remote_addr"
        },
        "proxy-rewrite": {
            "scheme": "https"
        }       
    },
    "upstream": {
        "desc": "guoxiuzhi_upstream",
        "nodes": {
            "127.0.0.1:18080": 1,
            "127.0.0.1:28080": 2
        },
        "type": "roundrobin",
        "retries": 2,
        "checks": {
            "active": {
                "http_path": "/status",
                "healthy": {
                    "interval": 2,
                    "successes": 1
                },
                "unhealthy": {
                    "interval": 1,
                    "http_failures": 2
                },
                "req_headers": ["User-Agent: curl/7.29.0"]
            },
            "passive": {
                "healthy": {
                    "http_statuses": [200, 201],
                    "successes": 3
                },
                "unhealthy": {
                    "http_statuses": [500],
                    "http_failures": 3,
                    "tcp_failures": 3
                }
            }
        }
    }
}'

注意:

  1. 由于我們的后端springboot是https訪問的,所以配置了proxy-rewrite插件的schema,官方文檔和api調用的單詞拼寫錯誤都是scheme,所以要將錯就錯。
  2. X-API-KEY: edd1c9f034335f136f87ad84b625c8f1是admin賬號的key,可以在conf\config.yaml的找到對應的值。
      name: "admin"
      key: edd1c9f034335f136f87ad84b625c8f1
      role: admin  

8.2 成功執行上面語句后輸出

{"node":{"value":{"priority":0,"plugins":{"limit-count":{"time_window":60,"count":2,"rejected_code":503,"key":"remote_addr","policy":"local"},"proxy-rewrite":{"scheme":"https"}},"uris":["\/*"],"upstream":{"retries":2,"hash_on":"vars","type":"roundrobin","nodes":{"127.0.0.1:18080":1,"127.0.0.1:28080":2},"desc":"guoxiuzhi_upstream","checks":{"active":{"unhealthy":{"http_statuses":[429,404,500,501,502,503,504,505],"interval":1,"timeouts":3,"http_failures":2,"tcp_failures":2},"concurrency":10,"http_path":"\/status","healthy":{"successes":1,"interval":2,"http_statuses":[200,302]},"req_headers":["User-Agent: curl\/7.29.0"],"https_verify_certificate":true,"timeout":1,"type":"http"},"passive":{"unhealthy":{"http_failures":3,"http_statuses":[500],"tcp_failures":3,"timeouts":7},"healthy":{"http_statuses":[200,201],"successes":3},"type":"http"}}}},"createdIndex":37,"key":"\/apisix\/routes\/016","modifiedIndex":37},"prevNode":{"value":"{\"priority\":0,\"plugins\":{\"limit-count\":{\"time_window\":60,\"count\":2,\"rejected_code\":503,\"key\":\"remote_addr\",\"policy\":\"local\"},\"proxy-rewrite\":{\"scheme\":\"https\"}},\"uris\":[\"\\\/*\"],\"upstream\":{\"retries\":2,\"nodes\":{\"127.0.0.1:18080\":1,\"127.0.0.1:28080\":2},\"hash_on\":\"vars\",\"checks\":{\"active\":{\"unhealthy\":{\"http_statuses\":[429,404,500,501,502,503,504,505],\"interval\":1,\"timeouts\":3,\"http_failures\":2,\"tcp_failures\":2},\"type\":\"http\",\"http_path\":\"\\\/status\",\"healthy\":{\"successes\":1,\"interval\":2,\"http_statuses\":[200,302]},\"req_headers\":[\"User-Agent: curl\\\/7.29.0\"],\"timeout\":1,\"https_verify_certificate\":true,\"concurrency\":10},\"passive\":{\"unhealthy\":{\"http_failures\":3,\"http_statuses\":[500],\"tcp_failures\":3,\"timeouts\":7},\"type\":\"http\",\"healthy\":{\"successes\":3,\"http_statuses\":[200,201]}}},\"desc\":\"guoxiuzhi_upstream\",\"type\":\"roundrobin\"}}","createdIndex":36,"key":"\/apisix\/routes\/016","modifiedIndex":36},"action":"set"}

8.3 使配置熱部署執行

curl http://127.0.0.1:9080/apisix/admin/plugins/reload -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT

返回:
done

8.4 查看狀態

http://101.133.227.13:9080/apisix/status
返回

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379