AWS Lambda笔记-内容分发(CDN)-7

本章使用CloudFormation来实现自定义域名并且使用AWS的SSL证书。同时利用AWS的CloudFront(AWS的CDN服务)让API网关实现多点接入,让用户在最近的CDN节点连接,从而加快用户和API之间的通讯速度。

  1. 配置CloudFront
  2. 配置自定义域名
  3. 关联SSL证书

工程说明

  1. 工程目录结构及java,build.gradle文件与AWS Lambda教程-自动部署-5 差不多唯一区别是build.gradle中将us-east-2改成us-east-1,本章主要是 cloudformation.tempalte 增加几段json块。
  2. 这边使用自定义域名,并且使用ACM证书(AWS Certificate Manager (ACM) )目前支持的地区仅us-east-1(弗吉尼亚北部)

cloudformation.tempalte中AWS组件关联关系,完整配置笔记的最后部分。


cloudformation.tempalte各AWS组件关联关系图

1. 配置CloudFront(CDN)

CloudFront都是例行公事的配置,这里主要有HTTP的版本,原站信息,缓存方式,是否支持压缩,被允许的HttpMethod类型,具体Forward信息等,详细看以下配置及具体备注。

"CloudformationDistribution": {
      //CDN配置分发,它告知CloudFront 从何处传输内容,并如何跟踪和管理内容传输的详细信息。
      "Type": "AWS::CloudFront::Distribution",
      "Properties": {
          "Enabled": "true",  //启用该资源
          "HttpVersion": "http2", //支持版本
          //此分配的源信息的复杂类型。用于描述CloudFront从中获取文件的S3 存储桶、
          //HTTP服务器、或其他服务器。
          "Origins": [  
            {
              "DomainName": { //允许开发者使用内建函数Fn::Sub和其他资源变量合成域名
                "Fn::Sub": "${RestApi}.execute-api.${AWS::Region}.amazonaws.com"
              },
              "OriginPath": "/production",  //源中的目录请求内容
              "Id": "APIGATEWAY", //源或源组的唯一标识符。
              "CustomOriginConfig": { //配置为网站终端节点的自定义源或S3存储桶
                "OriginProtocolPolicy": "https-only"  //要应用至源的源协议策略
              }
            }
          ],
          //描述缓存行为
          "DefaultCacheBehavior": {
            //当请求使用默认缓存行为时,CloudFront将请求路由到的源的ID值
            "TargetOriginId": "APIGATEWAY", 
            "Compress": true, //自动压缩此缓存行为的某些文件
            "AllowedMethods": [ //被允许的方法
              "DELETE",
              "GET",
              "HEAD",
              "OPTIONS",
              "PATCH",
              "POST",
              "PUT"
            ],
            "ForwardedValues": {  //处理查询字符串、Cookie 和 HTTP 标头
              //如果为 QueryString 指定 true,并且没有为 QueryStringCacheKeys 
              //指定任何值,CloudFront 会将所有查询字符串参数转发到来源,
              //并基于所有查询字符串参数进行缓存。根据拥有的查询字符串参数的个数和值,
              //这可能对性能产生不利影响,因为 CloudFront 必须将更多的请求转发到源。
              "QueryString": "true",
              "Cookies": {  //Cookie 转发到源
                "Forward": "none" //指定希望将哪些 Cookie 转发到此缓存行为的来源
              },
              //转发到此缓存行为的源的 Headers(如有)。对于您指定的标头,
              //CloudFront 还将缓存基于查看器请求中的标头值的指定对象的各个版本。
              "Headers": [  
                "Accept",
                "Content-Type",
                "Authorization"
              ]
            },
            "DefaultTTL": 0,  //TTL值
            "MaxTTL": 0,  //保留的最长时间
            "MinTTL": 0,  //如配置为将所有标头转发到源则必须为MinTTL指定0。
            //当请求与 TargetOriginId 中的路径模式匹配时,
            //查看器可用于访问 PathPattern 指定的来源中的文件的协议
            //redirect-to-https如果查看器提交HTTP请求,则CloudFront将向查看器返回 
            //HTTP 状态代码 301(永久移动)以及 HTTPS URL。然后,查看器会使用新的 
            //URL 重新提交请求。
            "ViewerProtocolPolicy": "redirect-to-https" 
          }
        }
      }
    }

以上配置好,我们可以部署(./gradlew deploy), 可以登陆CloudFront的控制台
,获取域名(d3se3kgs51ey11.cloudfront.net),可以dig测试下,我们的域名是否已经全球解析。第一个是国内dig的结果,域名解析到日本IP。第二个代理到美国域名解析的是美国IP。可以看出域名已经成功解析到各个区域。我们访问:https://d3se3kgs51ey11.cloudfront.net/test?value=hello+world,(d3se3kgs51ey11.cloudfront.net替换成你自己在CloudFront中域名)会相对会快一点点。

CloudFront的控制台

dig域名的结果:

dig d3se3kgs51ey11.cloudfront.net
;; ANSWER SECTION:
d3se3kgs51ey5f.cloudfront.net. 36 IN A 13.225.157.24
d3se3kgs51ey5f.cloudfront.net. 36 IN A 13.225.157.84
d3se3kgs51ey5f.cloudfront.net. 36 IN A 13.225.157.145
d3se3kgs51ey5f.cloudfront.net. 36 IN A 13.225.157.197

dig d3se3kgs51ey11.cloudfront.net
;; ANSWER SECTION:
d3se3kgs51ey5f.cloudfront.net. 300 IN A 13.227.53.123
d3se3kgs51ey5f.cloudfront.net. 300 IN A 13.227.53.231
d3se3kgs51ey5f.cloudfront.net. 300 IN A 13.227.53.49
d3se3kgs51ey5f.cloudfront.net. 300 IN A 13.227.53.61

2. 配置自定义域名

CloudFront配置成功后,我需要手动配置自定义域名的NS记录和SSL证书的认证。这些也可以通过CloudFormation模版自动化,不过这些操作都是一次性的,所有就不增加CloudFormation内容的复杂度,同时该域名没有配置邮箱,也不方面在申请SSL证书时通过邮件认证。

1)配置域名NS记录

进入AWS Route 53 管理页面https://console.aws.amazon.com/route53/home,点击左侧菜单“托管区域”。
重点:这边我们需要保存下, 托管区域:Z09377931HZWDHZB7ST9N,在后续配置中需要使用到。

创建托管区域

点击域名,可以看到该域名的NS,SOA记录。


NS记录

在自己的域名解析管理中增加NS记录


添加NS记录

配置生效后,我们为保障下一步顺利完成,验证下NS记录是否生效。
//dig ns 确认解析出来的为刚才配置的ns记录
dig ns serverless.kkkkkk.com
2)申请SSL证书并通过DNS认证

进入AWS Certificate Manger 页面 https://us-east-2.console.aws.amazon.com/acm/home ,点击“请求证书” ,按照提示
步骤 1: 添加域名
步骤 2:选择验证方法 (选择DNS验证)
步骤 3: 添加标签 (可以不操作)
步骤 4:审核并请求
步骤 5:验证
按步骤操作完成,回到“证书管理”页面,查看申请证书的域名,点击“在Route 53中创建记录” 这时会自动创建一个NS记录,等待一会儿域名状态从“等待审核” 变成 “已颁发”。

证书管理

成功之后,我们需要记录下ACM的ARN记录,如图:
image.png

接下来我们需要继续在cloudformation.template文件添加配置域名的A记录。

 "DNSRecord": {
      //可选注释、要更改的托管区域的名称和 ID,以及要创建的记录的值
      "Type": "AWS::Route53::RecordSetGroup",
      "Properties": {
        "Comment": "Z09377931HZWDHZB7ST9N在Route53上创建托管区域",
        ////要在其中创建记录的托管区域的ID,在“配置域名NS记录”中重点说明过
        "HostedZoneId": "Z09377931HZWDHZB7ST9N",
        "RecordSets": [ //一条记录的信息
          {
            "Name": {
              "Ref": "DomainName"
            },
            "Type": "A",  //DNS记录类型
            //仅限别名记录:有关您要将流量路由到的 AWS 资源
            //例如 CloudFront 分配或 Amazon S3 存储桶的信息。
            "AliasTarget": {  //CloudFront 分配
              //CloudFront分配,指定Z2FDTNDATAQYW2。
              //在创建将流量路由到CloudFront 分配的别名记录时,它始终是托管区域ID。
              "HostedZoneId": "Z2FDTNDATAQYW2", 
              "DNSName": {
                "Fn::GetAtt": [
                  "CloudformationDistribution",
                  "DomainName"
                ]
              }
            }
          }
        ]
      }
    }

Alias(别名)是Route53提供的强大功能之一,相比CName记录,别名记录可以直接指向AWS资源,例如ELB,CloudFront。别名在DNS中没有对应的概念,使用别名免费,而CName是付费服务。另外一个有点是Alias比CName少一步获取最终IP地址,减少解析的负担,继续发布工程(./gradlew deploy)发布成功后,我dig配置的域名可以发现解析的A记录多很多,到此自定义域名配置成功。但是使用http访问http://serverless.kkkkkk.com//test?value=hello+world,将放回403信息,提醒:The request could not be satisfied. 接下来我们将继续SSL证书配置。

dig serverlessbook.kkkkkk.com

; <<>> DiG 9.10.6 <<>> serverlessbook.kkkkkk.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29231
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;serverlessbook.kkkkkk.com. IN A

;; ANSWER SECTION:
serverlessbook.kkkkkk.com. 59 IN A 13.225.157.197
serverlessbook.kkkkkk.com. 59 IN A 13.225.157.84
serverlessbook.kkkkkk.com. 59 IN A 13.225.157.24
serverlessbook.kkkkkk.com. 59 IN A 13.225.157.145

;; Query time: 2468 msec
;; SERVER: 114.114.114.114#53(114.114.114.114)
;; WHEN: Sun Jun 07 15:33:07 CST 2020
;; MSG SIZE rcvd: 107

3. 关联SSL证书

这里只需要在 AWS::CloudFront::Distribution 的 Properties 增加:

"Aliases": [  //分配的 CNAME(备用域名)
{
  "Ref": "DomainName"
}
],
"ViewerCertificate": {  // SSL/TLS 配置
 //指定 ACM 证书 ARN,必须指定 MinimumProtocolVersion 和 SslSupportMethod 的值。 
 //使用 Aliases(备用域名或 CNAME),请指定分配接受来自哪些查看器的 HTTPS 连接.
 //分为sni-only(免费,到部分浏览器都支持,推荐) 和 vip(付费且需要单独申请)
"SslSupportMethod": "sni-only",
  //SSL证书申请中的ARN
"AcmCertificateArn": "arn:aws:acm:us-east-1:083845954160:certificate/0cc193a9-9489-47ce-b7b3-8213a4c434d1"
},

执行

~/.gradlew deploy

发布成功后,我们访问API就变成:https://serverless.kkkkkk.com/test?value=hello+world
现在这个API使用自定义域名且配置SSL,但是还没有权限控制,后续我们需要对该方法进行权限控制。



发布异常一

FAILURE: Build failed with an exception.

  • What went wrong:
    Execution failed for task ':awsCfnWaitStackComplete'.
    Status of stack serverlessbook is UPDATE_ROLLBACK_COMPLETE. It seems to be failed.

查看CloudFormation
]的“事件”提示具体的错误信息:

Property validation failure: [Encountered unsupported properties in {/DistributionConfig/ViewerCertificate}: [ACMCertificateArn]]
这个原因是ACMCertificateArn数名名的正确写法AcmCertificateArn,这说明CloudFormtion严格区分大小写。



cloudformation.tempalte 完整配置

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Parameters": {
        "DeploymentBucket": {
            "Type": "String",
            "Description": "S3 bucket name where built artifacts are deployed"
        },
        "ProjectVersion": {
            "Type": "String",
            "Description": "Project Version"
        },
        "DeploymentTime": {
            "Type": "String",
            "Description": "It is a timestamp value which shows the deployment time. Used to rotate sources."
        },
        "DomainName": {
            "Type": "String",
            "Description": "Domain Name to serve the application"
        }
    },
    "Resources": {
        "DeploymentLambdaRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "lambda.amazonaws.com"
                                ]
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Path": "/",
                "ManagedPolicyArns": [
                    "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
                ],
                "Policies": [
                    {
                        "PolicyName": "LambdaExecutionPolicy",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "lambda:PublishVersion",
                                        "apigateway:POST"
                                    ],
                                    "Resource": [
                                        "*"
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
        },
        "DeploymentLambda": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Role": {
                    "Fn::GetAtt": [
                        "DeploymentLambdaRole",
                        "Arn"
                    ]
                },
                "Handler": "serverless.handler",
                "Runtime": "nodejs12.x",
                "Code": {
                    "S3Bucket": {
                        "Fn::Sub": "serverless-arch-${AWS::Region}"
                    },
                    "S3Key": "serverless.zip"
                }
            }
        },
        "ApiGatewayCloudwatchRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "apigateway.amazonaws.com"
                                ]
                            },
                            "Action": "sts:AssumeRole"
                        }
                    ]
                },
                "Path": "/",
                "ManagedPolicyArns": [
                    "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
                ]
            }
        },
        "ApiGatewayAccount": {
            "Type": "AWS::ApiGateway::Account",
            "Properties": {
                "CloudWatchRoleArn": {
                    "Fn::GetAtt": [
                        "ApiGatewayCloudwatchRole",
                        "Arn"
                    ]
                }
            }
        },
        "RestApi": {
            "Type": "AWS::ApiGateway::RestApi",
            "Properties": {
                "Name": {
                    "Ref": "AWS::StackName"
                }
            }
        },
        "LambdaExecutionRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "Path": "/",
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "lambda.amazonaws.com"
                                ]
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "ManagedPolicyArns": [
                    "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
                ]
            }
        },
        "LambdaCustomPolicy": {
            "Type": "AWS::IAM::Policy",
            "Properties": {
                "PolicyName": "LambdaCustomPolicy",
                "PolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Action": [
                                "s3:ListBuckets"
                            ],
                            "Resource": "*"
                        }
                    ]
                },
                "Roles": [
                    {
                        "Ref": "LambdaExecutionRole"
                    }
                ]
            }
        },
        "TestLambda": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Handler": "com.serverlessbook.lambda.test.Handler",
                "Runtime": "java8",
                "Timeout": "300",
                "MemorySize": "1024",
                "Description": "Test lambda",
                "Role": {
                    "Fn::GetAtt": [
                        "LambdaExecutionRole",
                        "Arn"
                    ]
                },
                "Code": {
                    "S3Bucket": {
                        "Ref": "DeploymentBucket"
                    },
                    "S3Key": {
                        "Fn::Sub": "artifacts/lambda-test/${ProjectVersion}/${DeploymentTime}.jar"
                    }
                }
            }
        },
        "TestResource": {
            "Type": "AWS::ApiGateway::Resource",
            "Properties": {
                "PathPart": "test",
                "RestApiId": {
                    "Ref": "RestApi"
                },
                "ParentId": {
                    "Fn::GetAtt": [
                        "RestApi",
                        "RootResourceId"
                    ]
                }
            }
        },
        "TestGetMethod": {
            "Type": "AWS::ApiGateway::Method",
            "Properties": {
                "HttpMethod": "GET",
                "RestApiId": {
                    "Ref": "RestApi"
                },
                "ResourceId": {
                    "Ref": "TestResource"
                },
                "AuthorizationType": "NONE",
                "RequestParameters": {
                    "method.request.querystring.value": "True",
                    "method.request.header.Accept": "True"
                },
                "MethodResponses": [
                    {
                        "StatusCode": "200"
                    }
                ],
                "Integration": {
                    "Type": "AWS",
                    "Uri": {
                        "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TestLambda.Arn}/invocations"
                    },
                    "IntegrationHttpMethod": "POST",
                    "RequestParameters": {
                        "integration.request.querystring.value": "method.request.querystring.value",
                        "integration.request.header.Accept": "method.request.header.Accept"
                    },
                    "RequestTemplates": {
                        "application/json": "{\"value\":\"$input.params('value')\"}"
                    },
                    "PassthroughBehavior": "NEVER",
                    "IntegrationResponses": [
                        {
                            "SelectionPattern": ".*",
                            "StatusCode": "200"
                        }
                    ]
                }
            }
        },
        "TestLambdaPermission": {
            "Type": "AWS::Lambda::Permission",
            "Properties": {
                "Action": "lambda:InvokeFunction",
                "FunctionName": {
                    "Ref": "TestLambda"
                },
                "Principal": "apigateway.amazonaws.com",
                "SourceArn": {
                    "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*"
                }
            }
        },
        "ApiDeployment": {
            "DependsOn": [
                "TestGetMethod"
            ],
            "Type": "Custom::ApiDeployment",
            "Properties": {
                "ServiceToken": {
                    "Fn::GetAtt": [
                        "DeploymentLambda",
                        "Arn"
                    ]
                },
                "RestApiId": {
                    "Ref": "RestApi"
                },
                "StageName": "production",
                "DeploymentTime": {
                    "Ref": "DeploymentTime"
                }
            }
        },
        "CloudformationDistribution": {
            "Type": "AWS::CloudFront::Distribution",
            "Properties": {
                "DistributionConfig": {
                    "Aliases": [
                        {
                            "Ref": "DomainName"
                        }
                    ],
                    "ViewerCertificate": {
                        "SslSupportMethod": "sni-only",
                        "AcmCertificateArn": "arn:aws:acm:us-east-1:083845954160:certificate/0cc193a9-9489-47ce-b7b3-8213a4c434d1"
                    },
                    "Enabled": "true",
                    "HttpVersion": "http2",
                    "Origins": [
                        {
                            "DomainName": {
                                "Fn::Sub": "${RestApi}.execute-api.${AWS::Region}.amazonaws.com"
                            },
                            "OriginPath": "/production",
                            "Id": "APIGATEWAY",
                            "CustomOriginConfig": {
                                "OriginProtocolPolicy": "https-only"
                            }
                        }
                    ],
                    "DefaultCacheBehavior": {
                        "TargetOriginId": "APIGATEWAY",
                        "Compress": true,
                        "AllowedMethods": [
                            "DELETE",
                            "GET",
                            "HEAD",
                            "OPTIONS",
                            "PATCH",
                            "POST",
                            "PUT"
                        ],
                        "ForwardedValues": {
                            "QueryString": "true",
                            "Cookies": {
                                "Forward": "none"
                            },
                            "Headers": [
                                "Accept",
                                "Content-Type",
                                "Authorization"
                            ]
                        },
                        "DefaultTTL": 0,
                        "MaxTTL": 0,
                        "MinTTL": 0,
                        "ViewerProtocolPolicy": "redirect-to-https"
                    }
                }
            }
        },
        "DNSRecord": {
            "Type": "AWS::Route53::RecordSetGroup",
            "Properties": {
                "Comment": "Z09377931HZWDHZB7ST9N在Route53上创建托管区域",
                "HostedZoneId": "Z09377931HZWDHZB7ST9N",
                "RecordSets": [
                    {
                        "Name": {
                            "Ref": "DomainName"
                        },
                        "Type": "A",
                        "AliasTarget": {
                            "HostedZoneId": "Z2FDTNDATAQYW2",
                            "DNSName": {
                                "Fn::GetAtt": [
                                    "CloudformationDistribution",
                                    "DomainName"
                                ]
                            }
                        }
                    }
                ]
            }
        } 
    }
}

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