OpenSAML 使用引導(dǎo) III: Service Provider 的實(shí)現(xiàn)之Artifact與斷言

前文OpenSAML 使用引導(dǎo) II : Service Provider 的實(shí)現(xiàn)之AuthnRequest介紹從Service Provider(SP)角度出發(fā),講解如何使用OpenSAML如申請(qǐng)身份鑒別請(qǐng)求,并從IDP出得到斷言的引用標(biāo)識(shí)——SAML Artifact,本文將繼續(xù)討論Artifact的具體意義,如何使用Artifact換取斷言信息,以及斷言的使用方法。

相關(guān)閱讀

  1. SAML2.0入門指南,
  2. OpenSAML 使用引導(dǎo) I : 簡(jiǎn)介

源碼地址:https://github.com/sunrongxin7666/OpenSAML-ref-project-demo-v3.git

更新 2017-11-20
回答網(wǎng)友xiajale的提問(wèn):為什么使用Artifact Binding,詳情請(qǐng)見(jiàn)第六節(jié)

4. 第四步和第五步:The Artifact and Artifact Resolution

Artifact Resolve
Artifact Resolve Request

這里跳過(guò)了關(guān)于第三步身份認(rèn)證的討論,這是因?yàn)殛P(guān)于用戶身份的認(rèn)證完全取決于idp,和SAML協(xié)議本身沒(méi)有關(guān)系。

當(dāng)用戶通過(guò)身份鑒別之后,Idp會(huì)為認(rèn)證信息(斷言)分配一個(gè)標(biāo)識(shí),這個(gè)標(biāo)識(shí)被稱為ArtifactArtifact將以URL參數(shù)的形式和用戶一起被發(fā)送回SP。

SP為了通過(guò)Artifact得到真正的認(rèn)證信息,需要構(gòu)建ArtifactResolve對(duì)象,ArtifactResolve中包含ArtifactArtifactResolve通過(guò)SOAP協(xié)議發(fā)送給Idp,Idp返回ArtifactResolveResponse,其中就包含了SAML Assertion,即認(rèn)證信息。
由此可見(jiàn)Artifact對(duì)象至關(guān)重要,下面就來(lái)討論Artifact的細(xì)節(jié)。

Artifact

Artifact是對(duì)一個(gè)SAML對(duì)象的引用。SAML規(guī)定Artifact中必須包括以下內(nèi)容:

Artifact

  • TypeCode規(guī)定了當(dāng)前Artifact的類型,不同類型的Artifact對(duì)于Remaining Artifact的格式有著不同的要求;
  • EndPointIndex代表著metadata中定義的Web服務(wù)終端標(biāo)識(shí),在用Artifact兌換真正的SAML對(duì)象時(shí)使用;
  • Artifact是這些部分Base64編碼后的連續(xù)字節(jié);
  • 推薦的Artifact類型是type4
Artifact-type4
  • SourceID是發(fā)送者實(shí)體的ID被SHA-1哈希值;
  • MessageHandle是發(fā)送方對(duì)于SMAL消息真實(shí)的引用,為至少16位隨機(jī)標(biāo)識(shí),不足20位的應(yīng)該補(bǔ)全;

Artifact綁定(傳輸)

Artifact綁定是傳輸SAML信息(借助于其引用Artifact)的一種方式,通過(guò)HTTP客戶端(如瀏覽器)來(lái)工作。傳遞Artifact有兩種方式,HTTP POST和HTTP重定向。隨后Artifact來(lái)?yè)Q取真正的SAML消息,這一步靠SP和IDP可信信道來(lái)完成,比如SOAP信道。Artifact綁定的使用是因?yàn)镾AML消息中有敏感信息,直接通過(guò)瀏覽器傳送SAML消息并不安全。所以通過(guò)HTTP重定向傳輸Artifact,也就是說(shuō)Artifact被當(dāng)成URL中的參數(shù)傳輸。

創(chuàng)建ArtifactResolve

當(dāng)SP得到Artifact之后,根據(jù)Artifact構(gòu)造ArtifactResolve來(lái)請(qǐng)求真正的SAML消息。

Artifact artifact = OpenSAMLUtils.buildSAMLObject(Artifact.class); 
artifact.setArtifact(httpServletRequest.getParameter("SAMLart"));

然后構(gòu)建ArtifactResolve對(duì)象

ArtifactResolve artifactResolve = OpenSAMLUtils.buildSAMLObject(ArtifactResolve.class);

接下來(lái)開(kāi)始這是ArtfactResolve的各種屬性:

  • Issuer:發(fā)送方的身份表示,同
    AuthnRequest中的issuer;
Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
issuer.setValue(SPConstants.SP_ENTITY_ID);
authnRequest.setIssuer(issuer);
  • Time of the Request
artifactResolve.setIssueInstant(new DateTime());
  • ID of the request:
artifactResolve.setID(OpenSAMLUtils.generateSecureRandomId());
  • destination URL
artifactResolve.setDestination(getIPDArtifactResloveDestination());

最后將Artifact加入其中:

artifactResolve.setArtifact(artifact);

形成的Artifact Request XML如下:


Artifact Request

使用SOAP協(xié)議發(fā)送 ArtifactResolve

SOAP簡(jiǎn)單的理解,就是這樣的一個(gè)開(kāi)放協(xié)議SOAP=RPC+HTTP+XML:采用HTTP作為底層通訊協(xié)議;RPC作為一致性的調(diào)用途徑,XML作為數(shù)據(jù)傳送的格式,允許服務(wù)提供者和服務(wù)客戶經(jīng)過(guò)防火墻在INTERNET進(jìn)行通訊交互。
淺談SOAP
SOAP 教程

RCP(Rmote Procedure Call):遠(yuǎn)端程序調(diào)用,像調(diào)用本地對(duì)象那樣調(diào)用遠(yuǎn)端的程序(方法),為同步過(guò)程,會(huì)擁塞調(diào)用代碼的執(zhí)行。

RPC vs REST : RPC,是面向服務(wù)的,關(guān)于行為和動(dòng)作;REST面向資源的,強(qiáng)調(diào)描述應(yīng)用程序的事務(wù)和名詞。和REST相比,SOAP的優(yōu)勢(shì)在于能保證事物的原子性和消息可靠性,且對(duì)于數(shù)據(jù)完整性和數(shù)據(jù)隱私性的有很完備標(biāo)準(zhǔn)。

  1. REST Vs SOAP,Soap 和 Rest 的區(qū)別
  2. WebService的兩種方式SOAP和REST比較
  3. REST與SAOP的比較

AuthnRequest類似,發(fā)送ArtifactResolve也是通過(guò)message context

MessageContext<ArtifactResolve> contextout 
    = new MessageContext<ArtifactResolve>();
contextout.setMessage(artifactResolve);

不過(guò)對(duì)于SOAP消息,沒(méi)有強(qiáng)制的內(nèi)容需要添加到環(huán)境上下文中,但是建議加入數(shù)據(jù)簽名以增強(qiáng)安全性。

SignatureSigningParameters sigParameters = new SignatureSigningParameters();
sigParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
sigParameters.setSigningCredential(SPCredentials.getCredential());
sigParameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);

SecurityParametersContext securityParametersContext = contextout.getSubcontext(SecurityParametersContext.class,true);
securityParametersContext.setSignatureSigningParameters(signatureSigningParameters);

因?yàn)樯婕暗絻煞降耐ㄐ牛€需要?jiǎng)?chuàng)建InOutOperationContext來(lái)處理輸入輸出的信息。

InOutOperationContext<ArtifactResponse, ArtifactResolve> context
    = new ProfileRequestContext<ArtifactResponse, ArtifactResolve>();
context.setOutboundMessageContext(contextout);

為了能發(fā)送SOAP消息,還需要設(shè)置SOAP Client。這個(gè)Client將會(huì)調(diào)用消息的處理器,編碼器以及解碼等來(lái)傳送消息。這些內(nèi)容將在下一章中詳談。

創(chuàng)建SOAP Client的具體做法為繼承AbstractPipelineHttpSOAPClient并實(shí)現(xiàn)newPipline方法,來(lái)返回管道發(fā)送消息。

AbstractPipelineHttpSOAPClient<SAMLObject, SAMLObject> soapClient = new AbstractPipelineHttpSOAPClient() {
    protected HttpClientMessagePipeline newPipeline() throws SOAPException {
        //創(chuàng)建輸入輸出用的編碼器和解碼器
        HttpClientRequestSOAP11Encoder encoder
        = new HttpClientRequestSOAP11Encoder();
        HttpClientResponseSOAP11Decoder decoder
        = new HttpClientResponseSOAP11Decoder();
        //創(chuàng)建管道
        BasicHttpClientMessagePipeline pipeline
        = new BasicHttpClientMessagePipeline(
                encoder,
                decoder
        );
        //為輸出的內(nèi)容簽名
        pipeline.setOutboundPayloadHandler(
        new SAMLOutboundProtocolMessageSigningHandler());
        return pipeline;
    }
}
// HTTP幫助SOAPClient編碼和解碼
HttpClientBuilder clientBuilder = new HttpClientBuilder(); soapClient.setHttpClient(clientBuilder.buildClient());
//發(fā)送soap消息
soapClient.send(IDPConstants.ARTIFACT_RESOLUTION_SERVICE, context);

SOAP消息發(fā)送之后,會(huì)同步等待Response返回或者超時(shí)。當(dāng)Response返回時(shí),SAML消息便可或得到:

return context.getInboundMessageContext().getMessage();

ArtifactResponse實(shí)例如下:

Artifact Resolve Response XML With Encrypted Assertion
加密斷言部分

關(guān)于如何加密斷言,將在下一個(gè)大章節(jié)中詳細(xì)分析。

使用 Message Handlers 處理 SAML 消息

新版本OpenSAML的一大特點(diǎn)就是消息處理器容器(collection of message handlers)。這些消息處理來(lái)處消息并提供豐富的方法,如驗(yàn)證消息的有效性,驗(yàn)證簽名,簽名等等。處理器一般在解碼器或者編碼器之前調(diào)用。以下是一些可用的消息處理器:

  • MessageLifetimeSecurityHandler:生命周期驗(yàn)證,要求SAMLMessageInfoContext包含issue time;
  • SAMLOutboundProtocolMessageSigningHandler: 輸出消息簽名,要求SecurityParametersContext包含singing參數(shù)
  • ReceivedEndpointSecurityHandler:驗(yàn)證消息目的地址,要求base message context包含SAML消息,必需的信息可以從中提取出來(lái);

消息處理器可以直接調(diào)用:

SAMLOutboundProtocolMessageSigningHandler handler = new SAMLOutboundProtocolMessageSigningHandler();
handler.initialize();
handler.invoke(context);

也可以在Client的實(shí)現(xiàn)中隱式調(diào)用,比如在PipelineHttpSOAPClient中那樣。

當(dāng)有多個(gè)處理器被調(diào)用時(shí),可以使用BasicMessageHandlerChain來(lái)批量初始化和調(diào)用。

List handlers = new ArrayList<MessageHandler>(); 
handlers.add(handler1);
handlers.add(handler2);

BasicMessageHandlerChain<ArtifactResponse> handlerChain = new BasicMessageHandlerChain<ArtifactResponse>();
handlerChain.setHandlers(handlers);

以下是使用消息處理器的實(shí)例:
創(chuàng)建環(huán)境上下文:

MessageContext context = new MessageContext<ArtifactResponse>();
context.setMessage(artifactResponse);

將消息配置給SAMLMessageInfoContext,獲得Issue內(nèi)容:

SAMLMessageInfoContext messageInfoContext 
    = context.getSubcontext(SAMLMessageInfoContext.class, true);
messageInfoContext.setMessageIssueInstant(
    artifactResponse.getIssueInstant());

設(shè)置MessageLifetimeSecurityHandler

MessageLifetimeSecurityHandler lsHandler = new MessageLifetimeSecurityHandler();
lsHandler.setClockSkew(1000);
lsHandler.setMessageLifetime(2000);
lsHandler.setRequiredRule(true);

設(shè)置ReceivedEndpointSecurityHandler

ReceivedEndpointSecurityHandler receivedEndpointSecurityHandler = new ReceivedEndpointSecurityHandler();
receivedEndpointSecurityHandler.setHttpServletRequest(request);

將以上處理器放入處理器鏈中,批量處理:

List handlers = new ArrayList<MessageHandler>();
handlers.add(lifetimeSecurityHandler); 
handlers.add(receivedEndpointSecurityHandler); 

BasicMessageHandlerChain<ArtifactResponse> handlerChain = new BasicMessageHandlerChain<ArtifactResponse>(); 
handlerChain.setHandlers(handlers); 

handlerChain.initialize(); 
handlerChain.doInvoke(context); 

5. 斷言Assertion

SAML斷言是認(rèn)證的實(shí)體,其包含關(guān)于用戶和身份鑒別的信息。以下來(lái)簡(jiǎn)單介紹下斷言的內(nèi)容。

斷言用來(lái)承載安全信息,接收者可以根據(jù)這樣的信息來(lái)做出安全訪問(wèn)控制決定。斷言中所包含的安全信息被稱為斷言聲明(assertion statement),共有三種類型:

  • 認(rèn)證聲明:包含關(guān)于用戶身份鑒別的信息,比如用戶被認(rèn)證身份的時(shí)間和方法;
  • 屬性聲明: 包含關(guān)于用戶相關(guān)的屬性信息,比如用戶的姓名、手機(jī)號(hào)、郵箱和地址等等。這些屬性被IDP定義為KEY—Value的形式
  • 授權(quán)決定聲明:確認(rèn)當(dāng)前用戶是否授權(quán)來(lái)訪問(wèn)一個(gè)特殊的資源。身份鑒別決定只能提供最基本的鑒別能力,更為復(fù)雜情況推薦使用XACML(一種用于決定請(qǐng)求/響應(yīng)的通用訪問(wèn)控制策略語(yǔ)言和執(zhí)行授權(quán)策略的框架)。

5.1 斷言的使用

斷言在接收時(shí)是經(jīng)過(guò)加密和簽名的,這里對(duì)其的時(shí)候都是假設(shè)已經(jīng)解密并通過(guò)簽名驗(yàn)證的。


斷言1

斷言2

斷言3

斷言里包含很多的信息,下面將會(huì)展示從斷言里提取需要的信息。

用戶身份驗(yàn)證的時(shí)間在AuthnStatement字段的AuthnInstant 屬性中

logger.info("Authentication instant: "
    +assertion.getAuthnStatements().get(0).getAuthnInstant());

用戶被認(rèn)證的方式在AuthnStatement子字段AuthnContextAuthnContextClassRef屬性值,標(biāo)識(shí)認(rèn)證的方式:

logger.info("Authentication method: "
    + assertion.getAuthnStatements().get(0).getAuthnContext()
    + .getAuthnContextClassRef().getAuthnContextClassRef());

AttributeStatement之中包含很多屬性,都是Key_Value的形式:

for(Attribute attribute :
    assertion.getAttributeStatements()
        .get(0).getAttributes()) {
    logger.info("Attribute name: " + attribute.getName());
    for (XMLObject attributeValue : 
        attribute.getAttributeValues())
    {
        logger.info("Attribute value:"
            + ((XSString)attributeValue).getValue());
    }
}

6. 為什么使用Artifact Binding模式

SAML協(xié)議中如何使用Artifact Binding模式請(qǐng)求并獲得斷言流程已經(jīng)為大家介紹完畢。不過(guò)有的讀者可能有疑問(wèn), 為什么要使用Artifact Binding模式,HTTP POST或者HTTP重定向直接獲取斷言信息不是更簡(jiǎn)單嗎?的確是更簡(jiǎn)單,但是這樣做有安全性的隱患

其實(shí)SAML2.0中支持很多種綁定方式,如下都方式在OpenSAML中都有實(shí)現(xiàn):

  • SAML SOAP Binding (based on SOAP 1.1)
  • Reverse SOAP (PAOS) Binding
  • HTTP Redirect (GET) Binding
  • HTTP POST Binding
  • HTTP Artifact Binding
  • SAML URI Binding

目前,基于瀏覽器的SSO中,HTTP POST和HTTP Redirect的模式用的最多,但是瀏覽器本身有很多的限制和缺陷,使得有時(shí)候不便直接通過(guò)瀏覽器獲得身份認(rèn)證斷言:

  1. URL中的查詢字符串(Query String)或者POST payload有長(zhǎng)度限制,無(wú)法裝下斷言信息(斷言信息是XML格式,在簽名加密之后往往很大);
  2. 可能被注入JavaScript腳本,易遭受攻擊,如跨站腳本攻擊和跨站請(qǐng)求攻擊;所以無(wú)法斷定瀏覽器發(fā)出的請(qǐng)求是合法的,斷言內(nèi)的一些敏感信息不希望暴露給瀏覽器;
  3. 很多網(wǎng)站現(xiàn)在都是前后端分離開(kāi)發(fā),如果通過(guò)瀏覽器獲得斷言信息,前端代碼還需要將斷言信息通過(guò)API發(fā)送給后端,增加了斷言信息在網(wǎng)絡(luò)中的傳輸流程,也增加斷言被竊取和篡改的風(fēng)險(xiǎn)(對(duì)于XML結(jié)構(gòu)的數(shù)字簽名有漏洞XML wrapping attacks)。

使用Artifact Binding就可以避免以上問(wèn)題:

  • 瀏覽器只是獲得斷言信息的引用(Artifact),而不是真正的斷言,避免敏感信息暴露給瀏覽器,從而防止瀏覽器中潛在的風(fēng)險(xiǎn)和被攻擊點(diǎn);
  • Artifact換取斷言的過(guò)程使用SOAP協(xié)議,不受HTTP請(qǐng)求中長(zhǎng)度的限制;
  • SP向IDP換取斷言的過(guò)程是服務(wù)器到服務(wù)器之間通信,可以使用HTTPS來(lái)確認(rèn)雙發(fā)的身份(瀏覽器中HTTPS往往只是單方向驗(yàn)證服務(wù)的身份,而沒(méi)有驗(yàn)證瀏覽器的),非法的SP將無(wú)法獲得真正的斷言信息,并通信的過(guò)程加密傳輸,保證斷言的安全;
  • Artifact是一次性,被使用過(guò)后就是無(wú)效的,無(wú)法重放攻擊;
  • 斷言信息的請(qǐng)求和使用相分離,減少對(duì)于瀏覽器的依賴,更適用于前后端分離的SP;

其實(shí)如果熟悉OAuth中的授權(quán)碼模式和默認(rèn)模式的區(qū)別,SAML中使用Artifact或是HTTP Redirect也是異曲同工,依據(jù)使用場(chǎng)景不同而定。

更多關(guān)于SAML協(xié)議的是實(shí)現(xiàn)的內(nèi)容,請(qǐng)參見(jiàn)本人編寫(xiě)的一系列教程文章,其介紹如何使用OpenSAML,歡迎閱讀指正:

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

推薦閱讀更多精彩內(nèi)容