iOS进行HTTPS适配深入理解与实践

题外话:

在WWDC 2016中,苹果宣布在今年年底应用提交到App Store将需要支持ATS,在2016年12月21日,苹果官方发布最新通知:『Supporting App Transport Security』。大致意思就是说:这个截止日期已经被延长,新的截止日期待定。

听到这个消息,许多程序猿们虎躯一震,终于可以放心的喘口气过个好年了,但是技多不压身嘛,而且早晚用的着,所以今天就来侃一侃怎么适配HTTPS。

当然,很多开发者可能会误解会所有在应用里面的请求都必须使用HTTPS,其实不完全是这样。苹果官方文档提供了很多细节的东西,告诉开发者如何让自己的应用适配HTTPS,本文将从 原理 , 官方文档 ,以及作者的 实践经验出发,详细介绍iOS中如何使用HTTS以及使用时需要注意的细节问题。

本文内容导航:

  • 1、HTTPS基本原理
  • 2、官方相关文档解读
  • 3、实战使用
  • 3.1 使用CA颁发的证书
  • 3.2 使用自签名的证书
  • 3.3 生成自签名的证书

正文开始:

本文主要是单向验证,如果你的项目需要双向验证,看这个教程 『iOS--AFNetworking2.6/3.0--HTTPS客户端与服务端双向认证』

不过即使是双向验证,我的建议是先先看看这篇博客,这会对你遇到的问题有所帮助~

1、HTTPS基本原理

首先我们以网络传输的7个层来快速的弄懂HTTP和HTTPS的基本区别:

HTTP传输
HTTPS传输

一眼就可以看出区别了吧,其实HTTPS就是在HTTP的基础上,在传输层和会话层之间了一个SSL层,简单来说都作用是负责数据的加解密,从而保证了数据都安全。

SSL(Security Socket Layer 安全套接层) 最初1994年Netscape开发,专门用于保护Web通讯、保护浏览器和服务器之间的通信,在客户和服务器之间提供服务器鉴别、可选客户鉴别和加密通信信道。使用TCP提供一种可靠的端对端的安全服务。

版本和历史

  • 1.0,不成熟
  • 2.0,基本上解决了Web通讯的安全问题
    Microsoft公司发布了PCT(Private Communication Technology),并在IE中支持
  • 3.0,1996年发布,增加了一些算法,修改了一些缺陷
    TLS 1.0(Transport Layer Security传输层安全协议, 也被称为SSL 3.1),1997年IETF发布了Draft,同时,Microsoft宣布放弃PCT,与Netscape一起支持TLS 1.0
    1999年,发布RFC 2246(The TLS Protocol v1.0)

苹果现在要求使用的是TLS1.2。待会我会继续提到这个问题。

SSL的主要功能

  • 客户端验证服务器
  • 客户段与服务器选择彼此支持的算法
  • 服务器验证客户端(可选)
  • 使用公开密钥算法产生共享的密钥

当然,HTTPS比较难以理解的还是它的协议,尤其是握手协议,考虑到篇幅的原因,这里不过多的讲解。详情请看 『AFNetworking之于https认证』 中关于HTTPS的认证过程。

2、官方相关文档解读

App Transport Security (ATS)在iOS 9.0之后就默认开启了。也就是我们之前请求HTTPS的时候NSAllowsArbitraryLoads这个key的值默认设置为NO了,而且这个键现在在NSAppTransportSecurity字典的第一层。

NSAppTransportSecurity : Dictionary {
    NSAllowsArbitraryLoads : Boolean
    NSAllowsArbitraryLoadsForMedia : Boolean
    NSAllowsArbitraryLoadsInWebContent : Boolean
    NSAllowsLocalNetworking : Boolean
    NSExceptionDomains : Dictionary {
        <domain-name-string> : Dictionary {
            NSIncludesSubdomains : Boolean
            NSExceptionAllowsInsecureHTTPLoads : Boolean
            NSExceptionMinimumTLSVersion : String
            NSExceptionRequiresForwardSecrecy : Boolean   // Default value is YES
            NSRequiresCertificateTransparency : Boolean
        }
    }
}

除了NSExceptionRequiresForwardSecrecy默认的值是YES。其它的默认的值都是NO

NSAppTransportSecurity字典分两个层级配置,前面四个
NSAllowsArbitraryLoadsNSAllowsArbitraryLoadsForMedia,
NSAllowsArbitraryLoadsInWebContenNSAllowsLocalNetworking 是对整个APP全局的配置。如果我们需要对某个域名有区分的对待就需要在NSExceptionDomains里面进行相应的配置。

  • NSAllowsArbitraryLoads
    设置为YES的话,就会使得除了开发者在NSExceptionDomains里面配置的域名以外所有的网络连接不受限制。
    如果你设置为YES的话,需要在提审核的时候说明这样做的原因。

  • NSAllowsArbitraryLoadsForMedia
    设置为YES的话,所有在APP里面使用AV Foundation framework加载的视频都不会被限制。如果不设置的话,就仅用于加载已加密的媒体,例如由FairPlay或安全HLS保护的文件,并且不包含个人信息。
    如果你设置为YES的话,同样也是需要在提审核的时候说明你这样做的原因。

  • NSAllowsArbitraryLoadsInWebContent
    如果你设置为YES的话,系统会禁用对来自Web视图的请求的所有ATS限制,也就是你的WebView的请求不不一定需要HTTPS,APP就可以使用嵌入式浏览器来显示任意内容,但是应用的其他部分还是需要用ATS。
    如果你设置为YES的话,同样也是需要在提审核的时候说明你这样做的原因。

  • NSAllowsLocalNetworking
    设置为YES的话就允许加载本地资源。

  • NSExceptionDomains
    NSExceptionDomains其实是相当于NSAllowsArbitraryLoads的一个子集。后者是全局的作用,而前者主要是用于对某些域名的限制作用。他的主要作用其实就是用于们自签名的证书,具体使用细节我会在后面具体介绍。NSExceptionDomains字典里面各键的值意义如下:

    • NSIncludesSubdomains
      默认为NO,如果设置为YES,则表示当前设置域名的所有子域名也使用同样的配置。

    • NSExceptionAllowsInsecureHTTPLoads
      允许不安全的HTTP请求,这里所谓的不安全,不代表改变了 Transport Layer Security (TLS)或是事HTTPS的请求。所谓的不安全主要是因为使用自签名的证书,没有经过CA认证所以苹果并不知道是不是安全的,如果开发者允许那么苹果也允许加载。设置为YES,在审核的时候你需要说明原因。

    • NSExceptionMinimumTLSVersion
      这个属性用于表面你的HTTPS的TLS版本,因为苹果默认是支持TLS1.2,所以如果你使用了较低的版本你你需要自己指明。设置为这个键后,在审核的时候你也需要说明原因。

    • NSExceptionRequiresForwardSecrecy
      如果设置为NO,则允许不支持完全前向保密(PFS)的TLS密码(对于指定的域名)。 默认值为YES。关于完全正向保密,可以看这篇文章 『TLS完美前向保密 perfect forward secrecy』

以上这么多内容,对于iOS开发者来自说最重要的信息就是有五个键,如果开发者不使用默认值,则需要在审核的时候进行说明。这五个键是NSAllowsArbitraryLoadsNSAllowsArbitraryLoadsForMediaNSAllowsArbitraryLoadsInWebContentNSExceptionAllowsInsecureHTTPLoadsNSExceptionMinimumTLSVersion

这对于嵌入了很多网页或是视频的APP来说感觉会比较麻烦一些。

3、实战使用

3.1 使用CA颁发的证书

如果想要简单实用,公司又壕气,那就推荐使用这种方法。就是花钱买一个CA机构颁发的证书,也可以是CA机构授权二级或是三级机构颁发的证书,国内很多的颁发证书的公司,可以直接找他们买。

证书的算法使用RSA还是圆锥曲线(ECC)并没有太大区别,我见过的大部分是RSA算法的。

弄好证书后,丢给后台,让他们搭建HTTPS的服务器,理论上iOS端只需要修改地址为HTTPS的地址就可以适配成功。但是还是做一些配置来保证万无一失吧。

如果项目使用的是AFNetWorking 3.0的话。只需要如下几句代码就可以搞定。

NSString *urlString = @"https://www.apple.com";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
[securityPolicy setValidatesDomainName:YES];
manager.securityPolicy = securityPolicy;
manager.responseSerializer = [AFHTTPResponseSerializer serializer];

// request
[manager GET:urlString 
  parameters:nil 
  progress:nil
  success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {   
    NSDictionary * array = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:nil];
    NSLog(@"OK === %@",array);
    NSString *htmlString = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding];
    NSLog(@"%@",htmlString);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
      NSLog(@"error ==%@",error.description);
    }];

不过这里最值得关注的是AFSSLPinningModeNone,我们点进去看AF的源码的时候,就会发现这是一个枚举,主要包括了三种类型。

enum {
 AFSSLPinningModeNone,
 AFSSLPinningModePublicKey,
 AFSSLPinningModeCertificate,
 }

 `AFSSLPinningModeNone`
 Do not used pinned certificates to validate servers.

 `AFSSLPinningModePublicKey`
 Validate host certificates against public keys of pinned certificates.

 `AFSSLPinningModeCertificate`
 Validate host certificates against pinned certificates.
*/
  • AFSSLPinningModeNone表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,是不会通过的。
  • AFSSLPinningModePublicKey是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
  • AFSSLPinningModeCertificate表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。

如果进去查看AFSecurityPolicy.m里面的源码。可以看到设置为 AFSSLPinningModeNone,其实AFNetworking并没有做什么操作。

switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }

            // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);

            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }

            return NO;
        }
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }

也就是其实我们可以不用这句代码也可以正常访问。换言之,后台配好HTTPS,前端什么也不用干,改个域名即可。当然为了安全考虑建议使用AFSSLPinningModePublicKey或者是AFSSLPinningModeCertificate

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
[securityPolicy setValidatesDomainName:YES];
manager.securityPolicy = securityPolicy;

如果你使用后面两种模式你的工程里面需要导入cer证书文件。这个文件的路径随意,AFNetworking会自动替你寻找。如果你觉得不放心也可以使用下面的代码直接指定文件。

AFNetworking 3.0:

NSData *certData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"12306" ofType:@"cer"]];
NSSet *cerSet  = [NSSet setWithObject:certData];
if(certData){
  [securityPolicy setPinnedCertificates:cerSet];
}

不是3.0版本的话需要使用数组:

NSData *certData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"12306" ofType:@"cer"]];
if(certData){
  [securityPolicy setPinnedCertificates:@[cerSet]];
}

这样就可以去测试看看能不能跑起来了~

3.2 使用自签名的证书

使用自签名的证书比使用CA的要复杂一些。国内最著名的使用自签名证书的就是12306了。我们可以看看12306的证书文件。
首先我们需要下载它证书,对于网络上支持HTTPS的网站我们都可以通过下列方式获取相应的证书。

openssl s_client -connect kyfw.12306.cn:443 </dev/null 2>/dev/null | openssl x509 -outform DER > 12306.cer

openssl s_client -connect www.apple.com:443 </dev/null 2>/dev/null | openssl x509 -outform DER > apple.cer

执行命令后就会在当前目录下生成一个12306.cer的文件,我们把文件拷贝到Xcode里面可以查看相应的证书信息。

12306.cer

这个SRCA是12306自己搞定一个证书机构,也是没有CA认证过的。所以也算是一个自签名证书。

为了方便测试,我这里使用12306的证书来讲解自签证书配置。首先我不作任何配置使用如下代码测试网络能否联通。

    NSString *urlString = @"https://kyfw.12306.cn/otn";

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
    [securityPolicy setAllowInvalidCertificates:YES];
    [securityPolicy setValidatesDomainName:YES];
    manager.securityPolicy = securityPolicy;

    //用于指定文件
//    NSData *certData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"12306" ofType:@"cer"]];
//    NSSet *cerSet  = [NSSet setWithObject:certData];
//    if(certData){
//        [securityPolicy setPinnedCertificates:cerSet];
//    }

    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    [manager GET:urlString
      parameters:nil
        progress:nil
         success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
             NSDictionary * array = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:nil];
             NSLog(@"OK === %@",array);
             NSString *htmlString = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding];
             NSLog(@"%@",htmlString);
         } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
             NSLog(@"error ==%@",error.description);
         }];

可以看到结果如下:


打印报错

我们并不能正常访问1230网站,我们使用源码的方式打开plist文件:


打开plist文件

然后在里面添加如下配置:

    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>kyfw.12306.cn</key>
            <dict>
                <key>NSIncludesSubdomains</key>
                <true/>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
            </dict>
        </dict>
    </dict>

再次运行程序访问12306,可以看到成功加载到了数据:


打印成功

关于这个配置文件怎么配置呢。如果不是很清楚的话,建议使用苹果提供的工具:

/usr/bin/nscurl --ats-diagnostics [--verbose] URL

比如我测试苹果的网站(我只截取了部分信息):

hly:Desktop jianquan$  /usr/bin/nscurl --ats-diagnostics --verbose https://www.apple.com
Starting ATS Diagnostics

Configuring ATS Info.plist keys and displaying the result of HTTPS loads to https://www.apple.com.
A test will "PASS" if URLSession:task:didCompleteWithError: returns a nil error.
================================================================================

Default ATS Secure Connection
---
ATS Default Connection
ATS Dictionary:
{
}
Result : PASS

---
TLSv1.1 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.apple.com" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.1";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.0 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.apple.com" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.0";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

Result : PASS表明我可以使用推荐的键值对来适配HTTPS。对于自己的域名我们也可以先让后台配置好HTTPS之后,然后以上方法,就能更快更好的适配了。

对于自签的证书我们按照上面的教程配置就好。但是你会问我,BB可半天,证书在那里呢?别急下面一节我专门讲解如何生成自己的证书。

3.3 生成自签名的证书

我在大二的时候写过一篇文章,专门研究了数字证书的相关东西 『数字证书及其简单数字签名的实现(java实现)』

首先请确保你的电脑安装了jdk,没有的话请到官网下载安装。Mac或是Windows下生成证书的方式都是一样的。java的跨平台性。

安装好java后,打开终端,生成相应的证书:

keytool -genkey -alias JoySeeDog -keyalg RSA -keysize 1024  -keystore JoySeeDog -validity 365 

根据提示输入相关的信息,别的还好,主要是你的name这个不能错,需要是你的域名地址:

证书生成

这里面可以配置的参数包括如下,可以根据自己需求使用。

 -alias <alias>                  要处理的条目的别名
 -keyalg <keyalg>                密钥算法名称
 -keysize <keysize>              密钥位大小
 -sigalg <sigalg>                签名算法名称
 -destalias <destalias>          目标别名
 -dname <dname>                  唯一判别名
 -startdate <startdate>          证书有效期开始日期/时间
 -ext <value>                    X.509 扩展
 -validity <valDays>             有效天数
 -keypass <arg>                  密钥口令
 -keystore <keystore>            密钥库名称
 -storepass <arg>                密钥库口令
 -storetype <storetype>          密钥库类型
 -providername <providername>    提供方名称
 -providerclass <providerclass>  提供方类名
 -providerarg <arg>              提供方参数
 -providerpath <pathlist>        提供方类路径
 -v                              详细输出
 -protected                      通过受保护的机制的口令

然后会提示你输入相应的信息,如下图所示:


信息输入

然后可以使用如下命令查看相应的信息:

keytool -list -v -keystore JoySeeDog 

相关信息如下:


查看相关信息

确认证书的相关信息没有问题后,就可以导出相应的证书了。

keytool -export -alias JoySeeDog -file JoySeeDog.cer -keystore JoySeeDog  -rfc

导出证书选项也是很多,看个人需求了。

 -rfc                            以 RFC 样式输出
 -alias <alias>                  要处理的条目的别名
 -file <filename>                输出文件名
 -keystore <keystore>            密钥库名称
 -storepass <arg>                密钥库口令
 -storetype <storetype>          密钥库类型
 -providername <providername>    提供方名称
 -providerclass <providerclass>  提供方类名
 -providerarg <arg>              提供方参数
 -providerpath <pathlist>        提供方类路径
 -v                              详细输出
 -protected                      通过受保护的机制的口令
导出证书

可以看到当前目录下出现了一个JoySeeDog和一个JoySeeDog.cer文件。准确的说这个JoySeeDog文件在第一步就出现了,保存好这两个文件。以后也许会用到。

把证书拖入Xcode,可以看到崭新的证书做好了~

导出的最新证书

参考链接:『iOS进行HTTPS适配深入理解与实践』

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

推荐阅读更多精彩内容