理解Shiro身份認證授權原理

shiro安全框架的核心就是認證和授權,前面已談到關于restful的改造,本文主要談一下認證和授權過程,以及粗粒度和細粒度的授權等。
參考:https://blog.csdn.net/johnstrive/article/details/74741783

權限管理

基本上涉及到用戶參與的系統(tǒng)都要權限管理,權限管理屬于系統(tǒng)安全的范疇,權限管理實現對用戶訪問系統(tǒng)的控制,按照安全規(guī)則或者安全策略控制用戶可以訪問而且只能訪問自己被授權的資源
權限管理包括身份認證授權兩部分,簡稱認證授權。對于需要訪問控制的資源用戶首先經過身份認證,認證通過后用戶具有該資源的訪問權限方可訪問。

身份認證

判斷一個用戶是否為合法用戶的處理過程。最常用的簡單身份認證方式是系統(tǒng)通過核對用戶輸入的用戶名和口令,看其是否與系統(tǒng)中存儲的該用戶的用戶名和口令一致,來判斷用戶身份是否正確。對于采用指紋等系統(tǒng),則出示指紋;對于硬件Key等刷卡系統(tǒng),則需要刷卡。

認證關鍵對象

  • Subject:主體
    訪問系統(tǒng)的用戶,主體可以是用戶、程序等,進行認證的都稱為主體;
  • Principal:身份信息
    是主體(subject)進行身份認證的標識,標識必須具有唯一性,如用戶名、手機號、郵箱地址等,一個主體可以有多個身份,但是必須有一個主身份(Primary Principal)。
  • credential:憑證信息
    是只有主體自己知道的安全信息,如密碼、證書等。

授權

授權,即訪問控制,控制誰能訪問哪些資源。主體進行身份認證后需要分配權限方可訪問系統(tǒng)的資源,對于某些資源沒有權限是無法訪問的。

授權關鍵對象

授權可簡單理解為 whowhat(which) 進行 How 操作:

  • Who,即主體(Subject),主體需要訪問系統(tǒng)中的資源。
  • What,即資源(Resource),如系統(tǒng)菜單、頁面、按鈕、類方法、系統(tǒng)商品信息等。資源包括資源類型和資源實例,比如商品信息為資源類型,類型為t01的商品為資源實例,編號為001的商品信息也屬于資源實例。
  • How,權限/許可(Permission),規(guī)定了主體對資源的操作許可,權限離開資源沒有意義,如用戶查詢權限、用戶添加權限、某個類方法的調用權限、編號為001用戶的修改權限等,通過權限可知主體對哪些資源都有哪些操作許可。
    權限分為粗顆粒和細顆粒,粗顆粒權限是指對資源類型的權限,細顆粒權限是對資源實例的權限。

權限模型

對上節(jié)中的主體、資源、權限通過數據模型表示。

  • 主體(賬號、密碼)
  • 角色(角色名稱)
  • 主體和角色關系(主體id、角色id)
  • 權限(權限名稱、資源id)
  • 角色和權限關系(角色id、權限id)
  • 資源(資源id、訪問地址)

如下圖:


image.png

權限控制

基于角色的訪問控制

RBAC基于角色的訪問控制(Role-Based Access Control)是以角色為中心進行訪問控制,比如:主體的角色為總經理可以查詢企業(yè)運營報表,查詢員工工資信息等,訪問控制流程如下:

image.png

圖中的判斷邏輯代碼可以理解為:

if(主體.hasRole("總經理角色id")){
    查詢工資
}

缺點:以角色進行訪問控制粒度較粗,如果上圖中查詢工資所需要的角色變化為總經理和部門經理,此時就需要修改判斷邏輯為“判斷主體的角色是否是總經理或部門經理”,系統(tǒng)可擴展性差。
修改代碼如下:

if(主體.hasRole("總經理角色id") ||  主體.hasRole("部門經理角色id")){
    查詢工資
}

基于資源的訪問控制

RBAC基于資源的訪問控制(Resource-Based Access Control)是以資源為中心進行訪問控制,比如:主體必須具有查詢工資權限才可以查詢員工工資信息等,訪問控制流程如下:

image.png

上圖中的判斷邏輯代碼可以理解為:

if(主體.hasPermission("wage:query")){
    查詢工資
}

優(yōu)點:系統(tǒng)設計時定義好查詢工資的權限標識,即使查詢工資所需要的角色變化為總經理和部門經理也只需要將“查詢工資信息權限”添加到“部門經理角色”的權限列表中,判斷邏輯不用修改,系統(tǒng)可擴展性強。

粗顆粒度和細顆粒度

什么是粗顆粒度和細顆粒度

對資源類型的管理稱為粗顆粒度權限管理,即只控制到菜單、按鈕、方法,粗粒度的例子比如:用戶具有用戶管理的權限,具有導出訂單明細的權限。對資源實例的控制稱為細顆粒度權限管理,即控制到數據級別的權限,比如:用戶只允許修改本部門的員工信息,用戶只允許導出自己創(chuàng)建的訂單明細。

如何實現粗顆粒度和細顆粒度

對于粗顆粒度的權限管理可以很容易做系統(tǒng)架構級別的功能,即系統(tǒng)功能操作使用統(tǒng)一的粗顆粒度的權限管理
對于細顆粒度的權限管理不建議做成系統(tǒng)架構級別的功能,因為對數據級別的控制是系統(tǒng)的業(yè)務需求,隨著業(yè)務需求的變更業(yè)務功能變化的可能性很大,建議對數據級別的權限控制在業(yè)務層個性化開發(fā),比如:用戶只允許修改自己創(chuàng)建的商品信息可以在service接口添加校驗實現,service接口需要傳入當前操作人的標識,與商品信息創(chuàng)建人標識對比,不一致則不允許修改商品信息。

基于url攔截

基于url攔截是企業(yè)中常用的權限管理方法,實現思路是:將系統(tǒng)操作的每個url配置在權限表中,將權限對應到角色,將角色分配給用戶,用戶訪問系統(tǒng)功能通過Filter進行過慮,過慮器獲取到用戶訪問的url,只要訪問的url是用戶分配角色中的url則放行繼續(xù)訪問
如下圖:

image.png

Shiro中關鍵對象

  • Subject
    即主體,外部應用與subject進行交互,subject記錄了當前操作用戶,將用戶的概念理解為當前操作的主體,可能是一個通過瀏覽器請求的用戶,也可能是一個運行的程序。 Subject在shiro中是一個接口,接口中定義了很多認證授權相關的方法,外部程序通過subject進行認證授,而subject是通過SecurityManager安全管理器進行認證授權
  • SecurityManager
    即安全管理器,對全部的subject進行安全管理,它是shiro的核心,負責對所有的subject進行安全管理。通過SecurityManager可以完成subject的認證、授權等,實質上SecurityManager是通過Authenticator進行認證,通過Authorizer進行授權,通過SessionManager進行會話管理等。
    SecurityManager是一個接口,繼承了Authenticator, Authorizer, SessionManager這三個接口。
  • Authenticator
    即認證器,對用戶身份進行認證,Authenticator是一個接口,shiro提供ModularRealmAuthenticator實現類,通過ModularRealmAuthenticator基本上可以滿足大多數需求,也可以自定義認證器。
  • Authorizer
    即授權器,用戶通過認證器認證通過,在訪問功能時需要通過授權器判斷用戶是否有此功能的操作權限。
  • realm
    即領域,相當于datasource數據源,securityManager進行安全認證需要通過Realm獲取用戶權限數據,比如:如果用戶身份數據在數據庫那么realm就需要從數據庫獲取用戶身份信息。
    注意:不要把realm理解成只是從數據源取數據,在realm中還有認證授權校驗的相關的代碼。
  • SessionManager
    即會話管理,shiro框架定義了一套會話管理,它不依賴web容器的session,所以shiro可以使用在非web應用上,也可以將分布式應用的會話集中在一點管理,此特性可使它實現單點登錄。
  • SessionDAO
    即會話dao,是對session會話操作的一套接口,比如要將session存儲到數據庫,可以通過jdbc將會話存儲到數據庫。
  • CacheManager
    CacheManager即緩存管理,將用戶權限數據存儲在緩存,這樣可以提高性能。
  • Cryptography
    即密碼管理,shiro提供了一套加密/解密的組件,方便開發(fā)。比如提供常用的散列、加/解密等功能。

Shiro認證流程

image.png

自定義Realm

認證時需要自己實現realm去獲取特定的數據,如驗證賬號密碼等。

public class CustomRealm1 extends AuthorizingRealm {

    @Override
    public String getName() {
        return "customRealm1";
    }

    //支持UsernamePasswordToken
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    //認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {

        //從token中 獲取用戶身份信息
        String username = (String) token.getPrincipal();
        //拿username從數據庫中查詢
        //....
        //如果查詢不到則返回null
        if(!username.equals("zhang")){//這里模擬查詢不到
            return null;
        }

        //獲取從數據庫查詢出來的用戶密碼 
        String password = "123";//這里使用靜態(tài)數據模擬。。

        //返回認證信息由父類AuthenticatingRealm進行認證
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                username, password, getName());

        return simpleAuthenticationInfo;
    }

    //授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        // TODO Auto-generated method stub
        return null;
    }
}

注意,在SimpleAuthenticationInfo中第一個參數是Object類型,即你可以把User對象存入,在后面可以通過SecurityUtils.getSubject().getPrincipal()獲取用戶信息。

認證密碼加密

散列算法

一般用于生成一段文本的摘要信息,散列算法不可逆,將內容可以生成摘要,無法將摘要轉成原始內容。散列算法常用于對密碼進行散列,常用的散列算法有MD5、SHA。

一般散列算法需要提供一個salt(鹽)與原始內容生成摘要信息,這樣做的目的是為了安全性,比如:111111的md5值是:96e79218965eb72c92a549dd5a330112,拿著“96e79218965eb72c92a549dd5a330112”去md5破解網站很容易進行破解,如果要是對111111和salt(鹽,一個隨機數)進行散列,這樣雖然密碼都是111111加不同的鹽會生成不同的散列值。
代碼如下:

//md5加密,不加鹽
        String password_md5 = new Md5Hash("111111").toString();
        System.out.println("md5加密,不加鹽="+password_md5);

        //md5加密,加鹽,一次散列
        String password_md5_sale_1 = new Md5Hash("111111", "eteokues", 1).toString();
        System.out.println("password_md5_sale_1="+password_md5_sale_1);
        String password_md5_sale_2 = new Md5Hash("111111", "uiwueylm", 1).toString();
        System.out.println("password_md5_sale_2="+password_md5_sale_2);
        //兩次散列相當于md5(md5())

        //使用SimpleHash
        String simpleHash = new SimpleHash("MD5", "111111", "eteokues",1).toString();
        System.out.println(simpleHash);

在realm中使用

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {

        //用戶賬號
        String username = (String) token.getPrincipal();
        //根據用戶賬號從數據庫取出鹽和加密后的值
        //..這里使用靜態(tài)數據
        //如果根據賬號沒有找到用戶信息則返回null,shiro拋出異常“賬號不存在”

        //按照固定規(guī)則加密碼結果 ,此密碼 要在數據庫存儲,原始密碼 是111111,鹽是eteokues
        String password = "cb571f7bd7a6f73ab004a70322b963d5";
        //鹽,隨機數,此隨機數也在數據庫存儲
        String salt = "eteokues";

        //返回認證信息
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                username, password, ByteSource.Util.bytes(salt),getName());


        return simpleAuthenticationInfo;
    }

Shiro授權

授權流程

image.png

授權方式

Shiro 支持三種方式的授權:

  • 編程式:通過寫if/else 授權代碼塊完成:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有權限
} else {
//無權限
}
  • 注解式:通過在執(zhí)行的Java方法上放置相應的注解完成:
@RequiresRoles("admin")
public void hello() {
//有權限
}
  • JSP/GSP 標簽:在JSP/GSP 頁面通過相應的標簽完成:
<shiro:hasRole name="admin">
<!— 有權限—>
</shiro:hasRole>

Permission 鑒權方式

我們了解到了 Shiro 的Authorization有三種方式,作為細粒度化的 Authorization,Permission 同樣也支持粗粒度的 Authorization 的三種方式即代碼判斷,注解,JSP頁面校驗。

  • 代碼判斷/注解
@RequiresRoles("admin")
@RequiresPermissions("admin:view:*")
@RequestMapping(value="/admin")
public String AuthorizationOne () {
    Subject admin =SecurityUtils.getSubject();
    System.out.println("角色 " + admin.getPrincipal());
    admin.isPermitted("admin:view:*");
    System.out.println("角色 " + admin.getPrincipal()+" 是否擁有 admin:view:* 權限:"+admin.isPermitted("admin:view:*"));
    return "admin";
}
  • JSP 頁面校驗
<!-- 只有 user:view:* 權限才能顯示一下內容 -->
<shiro:hasPermission name="user:view:*">
    Only User has 'user:view:*' can access to those words
</shiro:hasPermission>
<br>
<!-- 只有 admin:view:* 權限才能顯示一下內容 -->
<shiro:hasPermission name="admin:view:*">
    Only Admin has 'admin:view:*' can access to those words
</shiro:hasPermission>

Shiro Authorization 大致可以被概述為實現了角色授權和權限授權

  • 角色授權:粗粒度授權,為當前Subject 做角色判定或賦予
  • 權限授權:細粒度授權,為當前Role 做權限判定或賦予。
  • 在細粒度授權時要重分理解 資源標識符:操作:對象實例ID 規(guī)則的定義的應用的實際場景。

權限字符串規(guī)則

權限一般是以字符串的形式表示的,權限字符串的規(guī)則是:“資源標識符:操作:資源實例標識符”,意思是對哪個資源的哪個實例具有什么操作,“:”是資源/操作/實例的分割符,, 表示操作的分割,* 表示任意資源/操作/實例。
如下:

用戶創(chuàng)建權限:user:create,或user:create:*
用戶修改實例001的權限:user:update:001
用戶實例001的所有權限:user:*:001

資源-操作-實例

資源-操作-實例 是 Shiro 做細粒度鑒權 persmission時的一種規(guī)則。

  • 擴展
    默認支持通配符權限字符串,: 表示資源/操作/實例的分割;, 表示操作的分割,* 表示任意資源/操作/實例。
  • 單個權限
    • user:query、user:edit。
    • 冒號是一個特殊字符,它用來分隔權限字符串的下一部件:第一部分是權限被操作的領域,第二部分是被執(zhí)行的操作。
    • 多個值:每個部件能夠保護多個值。因此,除了授予用戶 user:query和 user:edit 權限外,也可以簡單地授予他們一個:user:query, edit。
    • 還可以用 * 號代替所有的值,如:user:* , 也可以寫:*:query,表示某個用戶在所有的領域都有 query 的權限。
    • 例子
      • 單個資源多個權限 user:query user:add 多值 user:query,add
      • 單個資源所有權限 user:query,add,update,delete user:*
      • 所有資源某個權限 *:view
  • 實例級訪問控制
    • 規(guī)則: 資源標識符:操作:對象實例 ID
    • 這種情況通常會使用三個部件:域、操作、被付諸實施的實例。如:user:edit:manager
    • 也可以使用通配符來定義,如:user:edit:、user::、user::manager
    • 部分省略通配符:缺少的部件意味著用戶可以訪問所有與之匹配的值,比如:user:edit 等價于 user:edit :
      user 等價于 user:
      :*
    • 通配符只能從字符串的結尾處省略部件,也就是說 user:edit 并不等價于 user:*:edit
    • 例子
      • 單個實例的單個權限 printer:query:lp7200 printer:print:epsoncolor
        • 對資源printer的lp7200實例擁有query權限。
        • 對資源printer的epsoncolor實例擁有query權限。
      • 所有實例的單個權限 printer:print:*
        • 對資源printer的所有r實例擁有query權限。
      • 所有實例的所有權限 printer::
        • 對資源printer的1實例擁有所有權限。然后通過如下代碼判斷
          subject().checkPermissions(“printer:setting:1”, “printer:printe:2”);
      • 單個實例的所有權限 printer:*:lp7200
        • 對資源printer的lp7200實例擁有所有權限
      • 單個實例的多個權限 printer:query,print:lp7200
        • 對資源printer的lp7200實例擁有query,print權限

基于角色的授權

// 用戶授權檢測 基于角色授權
// 是否有某一個角色
System.out.println("用戶是否擁有一個角色:" + subject.hasRole("role1"));
// 是否有多個角色
System.out.println("用戶是否擁有多個角色:" + subject.hasAllRoles(Arrays.asList("role1", "role2")));

對應的check方法:

subject.checkRole("role1");
subject.checkRoles(Arrays.asList("role1", "role2"));

基于資源授權

// 基于資源授權
System.out.println("是否擁有某一個權限:" + subject.isPermitted("user:delete"));
System.out.println("是否擁有多個權限:" + subject.isPermittedAll("user:create:1",    "user:delete"));

對應的check方法:

subject.checkPermission("sys:user:delete");
subject.checkPermissions("user:create:1","user:delete");

自定義realm

與上邊認證自定義realm一樣,大部分情況是要從數據庫獲取權限數據,這里直接實現基于資源的授權。
在認證章節(jié)寫的自定義realm類中完善doGetAuthorizationInfo方法,此方法需要完成:根據用戶身份信息從數據庫查詢權限字符串,由shiro進行授權。

// 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        // 獲取身份信息
        String username = (String) principals.getPrimaryPrincipal();
        // 根據身份信息從數據庫中查詢權限數據
        //....這里使用靜態(tài)數據模擬
        List<String> permissions = new ArrayList<String>();
        permissions.add("user:create");
        permissions.add("user.delete");

        //將權限信息封閉為AuthorizationInfo

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for(String permission:permissions){
            simpleAuthorizationInfo.addStringPermission(permission);
        }

        return simpleAuthorizationInfo;
    }

自定義permission和RolePerminssion

通過addStringPermission 默認是用Permission的實現類封裝的 當然也可以實現自定義的Permission。

public class MyPermission implements Permission {

    String permissionCode;
    public MyPermission(String name) {
        permissionCode=name;
    }

    @Override
    public boolean implies(Permission permission) {
        //自定義比較
        // TODO Auto-generated method stub
        return false;
    }
}

當我們調用subject.isPermitted("user:update")會調用將指令傳達給SecurityManager,SecurityManager 再將指令傳達給授權管理類Authorizer,Authorizer會通過reaml獲得授權信息SimpleAuthorizationInfo如果我們返回的授權信息擁有角色 會調用RolePermissionResolver實現類的方法 將角色的權限追加到SimpleAuthorizationInfo(默認是沒有實現的)。

public class MyRolePermissionResolver  implements RolePermissionResolver{

    @Override
    public Collection<Permission> resolvePermissionsInRole(String roleString) {
        // TODO Auto-generated method stub
         return Arrays.asList((Permission)new MyPermission("menu:*")); 
    }

這里面是根據角色查詢權限

最終 遍歷SimpleAuthorizationInfo的權限信息 (我們的權限信息都封裝Permission接口實現類 調用implies方法進行比較 如果比較成功返回true 表示授權通過)自定義Permission的好處就是我們可以自定義匹配規(guī)則。

@RequiresPermissions 注解說明

@RequiresAuthentication

驗證用戶是否登錄,等同于方法subject.isAuthenticated() 結果為true時。

@RequiresUser

驗證用戶是否被記憶,user有兩種含義:
一種是成功登錄的(subject.isAuthenticated() 結果為true);
另外一種是被記憶的(subject.isRemembered()結果為true)。

@RequiresGuest

驗證是否是一個guest的請求,與@RequiresUser完全相反。
換言之,RequiresUser == !RequiresGuest。
此時subject.getPrincipal() 結果為null.

@RequiresRoles

例如:@RequiresRoles("aRoleName");
void someMethod();
如果subject中有aRoleName角色才可以訪問方法someMethod。如果沒有這個權限則會拋出異常AuthorizationException

@RequiresPermissions

例如: @RequiresPermissions({"file:read", "write:aFile.txt"} )
void someMethod();
要求subject中必須同時含有file:read和write:aFile.txt的權限才能執(zhí)行方法someMethod()。否則拋出異常AuthorizationException

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

推薦閱讀更多精彩內容