kerberos下HBase访问Zookeeper的ACL权限访问列表问题解决过程记录【转】

转自:http://www.aboutyun.com/thread-14977-1-1.html
最近公司HBase(CDH-4.6.0)遇到了一个麻烦问题,觉得有必要记录下整个解决的过程。

1、问题起因

用户在跑mapreduce任务,从hdfs读取文件想写入到hbase table的时候失败了(这是hbase提供的一种mapred能力)。这个问题发现在A环境(一个测试环境),自从启用了kerberos之后。运行了用户给的程序和自己写的sample之后,发现程序最后挂在NullPointerException上。这个NPE指示的是服务端的一个叫currentKey的变量为null。

org.apache.hadoop.hbase.ipc.ExecRPCInvoker$1@58e395e8,java.io.IOException: java.io.IOException: java.lang.NullPointerException
at  org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.createPassword(AuthenticationTokenSecretManager.java:129) 
at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.createPassword(AuthenticationTokenSecretManager.java:57)
at org.apache.hadoop.security.token.Token.<init>(Token.java:70)
at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.generateToken(AuthenticationTokenSecretManager.java:162)
at org.apache.hadoop.hbase.security.token.TokenProvider.getAuthenticationToken(TokenProvider.java:91) 
at sun.reflect.GeneratedMethodAccessor56.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.apache.hadoop.hbase.regionserver.HRegion.exec(HRegion.java:5610)
at org.apache.hadoop.hbase.regionserver.HRegionServer.execCoprocessor(HRegionServer.java:3918)
at sun.reflect.GeneratedMethodAccessor39.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.apache.hadoop.hbase.ipc.SecureRpcEngine$Server.call(SecureRpcEngine.java:311)

AuthenticationTokenSecretManager

@Override
 protected byte[] createPassword(AuthenticationTokenIdentifier identifier) {
   long now = EnvironmentEdgeManager.currentTimeMillis();
   AuthenticationKey secretKey = currentKey;  //currentKey赋给secretKey
   identifier.setKeyId(secretKey.getKeyId()); //NPE在这里抛出的,也就是currentKey为null
   identifier.setIssueDate(now);
   identifier.setExpirationDate(now + tokenMaxLifetime);
   identifier.setSequenceNumber(tokenSeq.getAndIncrement());
   return createPassword(WritableUtils.toByteArray(identifier),
       secretKey.getKey());
 }

2、问题定位

既然currentKey为null,那我们就去找它在哪里赋值的。阅读源码之后,了解到整个过程是这样的:
1.在开启kerberos之后,每个RegionServer都会有一个AuthenticationTokenSecretManager用来管理token。
2.这些manager中,只有一个leader,只有它能生产token,然后放到zookeeper里。其它manager通过感知zookeeper的变化来同步leader生产的token。leader通过竞争产生,谁先在ZK上创建 /hbase/tokenauth/keymaster 节点,谁就是leader。
AuthenticationTokenSecretManager$LeaderElector:

public void run() {
    zkLeader.start();
    zkLeader.waitToBecomeLeader();  //没有成为leader的人会一直阻塞在这里,直到感知到当前leader挂掉才会开始新一轮竞争
    isMaster = true;
    while (!stopped) {
      long now = EnvironmentEdgeManager.currentTimeMillis();
      // clear any expired
      removeExpiredKeys(); //清除过期的token,同时也把它从ZK上移除
      if (lastKeyUpdate + keyUpdateInterval < now) {  //默认的周期是1天
        // roll a new master key
        rollCurrentKey();  //就是这个函数产生新的token,替换currenKey
      }
      try {
        Thread.sleep(5000);
      } catch (InterruptedException ie) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Interrupted waiting for next update", ie);
        }
      }
    }
  }

AuthenticationTokenSecretManager:

synchronized void rollCurrentKey() {
    if (!leaderElector.isMaster()) {
      LOG.info("Skipping rollCurrentKey() because not running as master.");
      return;
    }
    long now = EnvironmentEdgeManager.currentTimeMillis();
    AuthenticationKey prev = currentKey;
    AuthenticationKey newKey = new AuthenticationKey(++idSeq,
        Long.MAX_VALUE, // don't allow to expire until it's replaced by a new key
        generateSecret());
    allKeys.put(newKey.getKeyId(), newKey);
    currentKey = newKey;           //滚动currentKey,置为newKey
    zkWatcher.addKeyToZK(newKey);  //把新的token放到zookeeper
    lastKeyUpdate = now;
    if (prev != null) {
      // make sure previous key is still stored
      prev.setExpiration(now + tokenMaxLifetime); //prev是原来的newKey,是不会过期的,当有新的newKey替代它后,它的期限默认设置是7天
      allKeys.put(prev.getKeyId(), prev);
      zkWatcher.updateKeyInZK(prev);
    }
  }

3.既然token是由leader生产的,除非没有leader,才会没人生产。验证这个想法,我在zookeeper和一些region server启动当天的日志里找到了证据:
a) zk中的 /hbase/tokenauth/keymaster 节点用来存放leader的信息,然后进入zookeeper-client查看了下,根本没这个节点。
b) 一些尚有保留集群启动当天日志的region server上找到了如下异常:

org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager: Zookeeper initialization failed
org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keys
at org.apache.zookeeper.KeeperException.create(KeeperException.java:113)
at org.apache.zookeeper.KeeperException.create(KeeperException.java:51)
at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:783)
at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.createNonSequential(RecoverableZooKeeper.java:421)
at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.create(RecoverableZooKeeper.java:403)
at org.apache.hadoop.hbase.zookeeper.ZKUtil.createWithParents(ZKUtil.java:1164)
at org.apache.hadoop.hbase.zookeeper.ZKUtil.createWithParents(ZKUtil.java:1142)
at org.apache.hadoop.hbase.security.token.ZKSecretWatcher.start(ZKSecretWatcher.java:58)
at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.start(AuthenticationTokenSecretManager.java:105)
at org.apache.hadoop.hbase.ipc.SecureRpcEngine$Server.startThreads(SecureRpcEngine.java:275)
at org.apache.hadoop.hbase.ipc.HBaseServer.start(HBaseServer.java:1650)
at org.apache.hadoop.hbase.regionserver.HRegionServer.startServiceThreads(HRegionServer.java:1728)
at org.apache.hadoop.hbase.regionserver.HRegionServer.handleReportForDutyResponse(HRegionServer.java:1105)
at org.apache.hadoop.hbase.regionserver.HRegionServer.run(HRegionServer.java:753)
at java.lang.Thread.run(Thread.java:662)

这是AuthenticationTokenSecretManager启动时候失败了,启动的时候会先在ZK上创建/hbase/tokenauth/keys这个目录(即便这个目录已经存在也会执行这个操作,这是一种保证),这个目录用来存放leader生成的token。结果大家都没有/hbase/tokenauth的权限,所以都失败了(NoAuth for /hbase/tokenauth/keys,这里的提示有点瑕疵,实际上/hbase/tokenauth没有权限导致的)。然而发生这样的严重错误,server的启动并没有被终止,而是继续运行下去,留下了隐患。
AuthenticationTokenSecretManager:

public void start() {
   try {
     // populate any existing keys
     this.zkWatcher.start(); //这里抛出的KeeperException 
     // try to become leader
     this.leaderElector.start(); //这里竞争leader,但是因为异常这里不会被执行,所以没有人去竞争leader
   } catch (KeeperException ke) {
     LOG.error("Zookeeper initialization failed", ke); //发生异常,仅仅是打印一条error信息,而没有abort。在Hbase的很多地方,发生这样的错误都是会abort server的。
   }
 }

4.错误原因就是/hbase/tokenauth权限问题,在zookeeper-client里查看了下它的权限是这样的:

[zk: localhost:2181(CONNECTED) 0] getAcl /hbase/tokenauth
'sasl,'hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM
: cdrwa

但很奇怪的是不管我切换什么账户也无法访问这个节点,想通过setAcl设置它的权限为anyone也是失败的。原因很显然,因为我不是“hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM”,我没任何权限操作。

Authentication is not valid : /hbase/tokenauth

可为什么4048这台机子也没能成为leader呢(问题[1])?

3、第一次解决(day 0)

尝试各种办法也无法获得/hbase/tokenauth的控制权,我们只好暂时通过在zookeeper配置文件zoo.cfg添加参数skipACL=yes,重启zookeeper,这样不会验证ACL。
重启hbase,触发AuthenticationTokenSecretManager.start,大家开始竞争成为leader,于是有了leader,leader是4048这台机子。
然后再通过zookeeper-client的setAcl命令把这个点的权限改成anyone,再关闭skipACL,重启zookeeper。
这些是我同事操作的,操作完之后集群一切正常,mapreduce也可以跑了。不过还有一个隐患,我注意到了/hbase/tokenauth/keys的权限也是4048专属,如果4048挂掉了,别人也无法顺利成为leader,但是想想它挂掉的概率比较低,等它挂掉再说吧,于是就没去理会了。

4、问题2 (day 1)

今天中午的时候,集群突然奔溃了,所有region server都挂掉了。 我上去查了一下日志,结果竟然和我昨天考虑到的隐患一样,4048挂掉了,然后其他人竞争leader的时候没有权限也挂掉了。4048为什么会挂掉(问题[2])? 当时我没怎么看4048的日志,不知道它为什么挂掉,只觉得很巧。
这是从4050这台机子的region server上截取的两条日志,它先是成为了leader,然后因为没有权限维护/hbase/tokenauth/keys,自然想访问里面的key也是失败的。其他机子挂掉的原因也一样。

2015-08-25 14:35:08,273 DEBUG org.apache.hadoop.hbase.zookeeper.ZKLeaderManager: Claimed the leader znode as 'SVR4050HW2285.hadoop.xxx.com,60020,1440397852179'
2015-08-25 14:35:08,288 FATAL org.apache.hadoop.hbase.regionserver.HRegionServer: ABORTING region server SVR4050HW2285.hadoop.xxx.com,60020,1440397852179: Unable to synchronize secretkey 3 in zookeeper
org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keys/3
at org.apache.zookeeper.KeeperException.create(KeeperException.java:113)
at org.apache.zookeeper.KeeperException.create(KeeperException.java:51)
at org.apache.zookeeper.ZooKeeper.setData(ZooKeeper.java:1266)
at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.setData(RecoverableZooKeeper.java:349)
at org.apache.hadoop.hbase.zookeeper.ZKUtil.updateExistingNodeData(ZKUtil.java:814)
at org.apache.hadoop.hbase.security.token.ZKSecretWatcher.updateKeyInZK(ZKSecretWatcher.java:197)
at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.rollCurrentKey(AuthenticationTokenSecretManager.java:257)
at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager$LeaderElector.run(AuthenticationTokenSecretManager.java:317)

5、第二次解决(day 1)

添加skipACL后重启ZK,重启HBase。就这样暂时保持skipACL开启,保证hbase正常运行。

6、思考(day 2)

我们总不能这样开着skipACL,这对资源隔离不是很友好。我查看了下HBase的ZKUtil.java的代码。
这是创建ZNode时候,创建ACL的函数。它对一些特定节点使用CREATOR_ALL_AND_WORLD_READABLE权限,其余使用CREATOR_ALL_ACL权限。前者是创建者有所有权限,其余人有只读权限。后者是创建者有所有权限。

private static ArrayList<ACL> createACL(ZooKeeperWatcher zkw, String node) {
   if (isSecureZooKeeper(zkw.getConfiguration())) {
     // Certain znodes are accessed directly by the client,
     // so they must be readable by non-authenticated clients
     if ((node.equals(zkw.baseZNode) == true) ||
         (node.equals(zkw.rootServerZNode) == true) ||
         (node.equals(zkw.masterAddressZNode) == true) ||
         (node.equals(zkw.clusterIdZNode) == true) ||
         (node.equals(zkw.rsZNode) == true) ||
         (node.equals(zkw.backupMasterAddressesZNode) == true) ||
         (node.startsWith(zkw.assignmentZNode) == true) ||
         (node.startsWith(zkw.masterTableZNode) == true) ||
         (node.startsWith(zkw.masterTableZNode92) == true)) {
       return ZooKeeperWatcher.CREATOR_ALL_AND_WORLD_READABLE;
     }
     return Ids.CREATOR_ALL_ACL;
   } else {
     return Ids.OPEN_ACL_UNSAFE;
   }
 }

/hbase/tokenauth及其子节点显然使用的是CREATOR_ALL_ACL权限。那4048创建了key,然后又挂掉的话,那其它机子显然不可能成为leader。这种权限设定似乎有点不科学。
因为B环境权限都很正常的,没出什么问题,我又对比了下A和B的权限和配置。
B leader生产的token的权限:

[zk: localhost:2181(CONNECTED) 4] getAcl /hbase/tokenauth/keys/67
'sasl,'hbase
: cdrwa

A leader生产的token的权限:

[zk: localhost:2181(CONNECTED) 1] getAcl /hbase/tokenauth/keys/2
'sasl,'hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM
: cdrwa

前者非常统一的使用hbase这个principal,后者则带上了hostname。
问题必定出在这里!

我又对比了hbase的zk-jaas.conf,没区别。这个配置文件里配置了访问zk的principal,它们都是带hostname的。

Client {
com.sun.security.auth.module.Krb5LoginModule required
useKeyTab=true
useTicketCache=false
keyTab="/etc/hbase.keytab"
principal="hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM";
};

可为什么B最后的principal却没带hostname,我又对比了zookeeper的配置文件zoo.cfg。
B的有下面两行设置:

kerberos.removeHostFromPrincipal=true
kerberos.removeRealmFromPrincipal=true

而A呢?居然也有。。。
和同事讨论了下,他告诉我A这两行配置不是一开始就有的,是后来加上去的,当时A最早上kerberos还出了很多问题。我瞬间就懂了,一切疑惑都解开了。
问题[1]:为什么4048这台机子也没能成为leader呢?
因为当初集群最早上kerberos启动的时候没加那两行remove配置,所以/hbase/tokenauth和/hbase/tokenauth/keys的权限都是归4048专属。后来因为出了问题,这两行配置被加上去,hbase重启。此时大家的principal都变成了hbase(包括4048),没有人能访问这个4048专属的目录。于是包括4048在内,没人成为leader。
问题[2]:4048为什么会挂掉?
这个是因为我们第一次解决的时候,只修复了/hbase/tokenauth而没有修复/hbase/tokenauth/keys,它的权限依然是4048所有。

[zk: localhost:2181(CONNECTED) 0] getAcl /hbase/tokenauth/keys
'sasl,'hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM
: cdrwa

当时重启hbase的时候还是开着skipACL的,所以leader顺利的在/hbase/tokenauth/keys下面创建了token,集群正常启动,一切正常。
然后我们关闭了skipACL,似乎也没有问题,可为什么恰好第二天就奔溃了?
因为leader去更新token的默认周期恰好是一天,第二天它想更新的时候因为没有/hbase/tokenauth/keys的权限而挂掉。
因为我们加了那两行remove配置,即使这个leader是4048,它也无法访问,道理同问题[1]。

这个证据也很好找。
这是第一次解决时,新写入的token,它的创建时间是24号下午2点半。

[zk: localhost:2181(CONNECTED) 3] stat /hbase/tokenauth/keys/3 
cZxid = 0x1900000097
ctime = Mon Aug 24 14:30:48 CST 2015
mZxid = 0x1c000000e8
mtime = Tue Aug 25 15:35:36 CST 2015
pZxid = 0x1900000097
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 42
numChildren = 0

这是leader挂掉的日志,时间是第二天下午2点半,集群奔溃也是在2点半左右,刚好间隔24小时左右。

2015-08-25 14:33:01,515 FATAL org.apache.hadoop.hbase.security.token.ZKSecretWatcher: Unable to synchronize master key 4 to znode /hbase/tokenauth/keys/4
org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keys/4
at org.apache.zookeeper.KeeperException.create(KeeperException.java:113)
at org.apache.zookeeper.KeeperException.create(KeeperException.java:51)
at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:783)
at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.createNonSequential(RecoverableZooKeeper.java:421)
at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.create(RecoverableZooKeeper.java:403)
at org.apache.hadoop.hbase.zookeeper.ZKUtil.createWithParents(ZKUtil.java:1164)
at org.apache.hadoop.hbase.zookeeper.ZKUtil.createSetData(ZKUtil.java:868)
at org.apache.hadoop.hbase.security.token.ZKSecretWatcher.addKeyToZK(ZKSecretWatcher.java:180)
at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.rollCurrentKey(AuthenticationTokenSecretManager.java:250)
at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager$LeaderElector.run(AuthenticationTokenSecretManager.java:317)
2015-08-25 14:33:01,516 FATAL org.apache.hadoop.hbase.regionserver.HRegionServer: ABORTING region server SVR4048HW2285.hadoop.xxx.com
       ,60020,1440397852099: Unable to synchronize secret key 4 in zookeeper

日志显示它想写入新的token 4失败而终止,昨天写入的是3。因为新的token的id比旧的大一,所以正好挂在想写入4的时候。

AuthenticationTokenSecretManager:

synchronized void rollCurrentKey() {
   if (!leaderElector.isMaster()) {
     LOG.info("Skipping rollCurrentKey() because not running as master.");
     return;
   }
   long now = EnvironmentEdgeManager.currentTimeMillis();
   AuthenticationKey prev = currentKey;
   AuthenticationKey newKey = new AuthenticationKey(++idSeq,  //新token的id比上一次大一
       Long.MAX_VALUE, // don't allow to expire until it's replaced by a new key
       generateSecret());
   allKeys.put(newKey.getKeyId(), newKey);
   currentKey = newKey;
   zkWatcher.addKeyToZK(newKey); //试图向zk写入新token
   lastKeyUpdate = now;
   if (prev != null) {
     // make sure previous key is still stored
     prev.setExpiration(now + tokenMaxLifetime);
     allKeys.put(prev.getKeyId(), prev);
     zkWatcher.updateKeyInZK(prev);
   }
 }

7、第三次解决(day 2)

修复zk上所有权限有问题的节点(设置权限为anyone),删除过期的token(这些token因为没有权限,没被人删除),关闭skipACL,重启zk。
因为已经添加了remove配置,现在不同region server访问zookeeper的principal都是一样的,不会再出现权限问题。
后记
为了保证不同region server访问zookeeper的principal一样,我们必须在zoo.cfg里添加remove配置,这种做法似乎不是特别科学。
因为作为hbase,你不能保证zookeeper里会有remove配置。假如zookeeper是另一个团队维护,他们觉得添加了这样的配置对其它app有影响呢?
事实上hbase作为client,zookeeper作为server,我们似乎可以给hbase配置统一的client身份?
zk-jaas.conf 类似这样:

Client {
  com.sun.security.auth.module.Krb5LoginModule required
  useKeyTab=true
  keyTab="/path/to/zkcli.keytab"
  storeKey=true
  useTicketCache=false
  principal="zkcli@<YOUR-REALM>";
};

而不是这样:

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

推荐阅读更多精彩内容