?? Golang Gin 輕量級 RBAC 鑒權(quán)庫

grbac

項目地址: https://github.com/storyicon/grbac

Grbac是一個快速,優(yōu)雅和簡潔的RBAC框架樟氢。它支持增強的通配符并使用Radix樹匹配HTTP請求豺瘤。令人驚奇的是辆床,您可以在任何現(xiàn)有的數(shù)據(jù)庫和數(shù)據(jù)結(jié)構(gòu)中輕松使用它愿卸。

grbac的作用是確保指定的資源只能由指定的角色訪問灵临。請注意,grbac不負責(zé)存儲鑒權(quán)規(guī)則和分辨“當(dāng)前請求發(fā)起者具有哪些角色”趴荸,更不負責(zé)角色的創(chuàng)建儒溉、分配等。這意味著您應(yīng)該首先配置規(guī)則信息发钝,并提供每個請求的發(fā)起者具有的角色顿涣。

grbac將Host波闹、PathMethod的組合視為Resource,并將Resource綁定到一組角色規(guī)則(稱為Permission)园骆。只有符合這些規(guī)則的用戶才能訪問相應(yīng)的Resource舔痪。

讀取鑒權(quán)規(guī)則的組件稱為Loader。grbac預(yù)置了一些Loader锌唾,你也可以通過實現(xiàn)func()(grbac.Rules,error)來根據(jù)你的設(shè)計來自定義Loader夺英,并通過grbac.WithLoader加載它晌涕。

1. 最常見的用例

下面是最常見的用例,它使用gin痛悯,并將grbac包裝成了一個中間件余黎。通過這個例子,你可以很容易地知道如何在其他http框架中使用grbac(比如echo载萌,iris惧财,ace等):

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/storyicon/grbac"
    "net/http"
    "time"
)

func LoadAuthorizationRules() (rules grbac.Rules, err error) {
    // 在這里實現(xiàn)你的邏輯
    // ...
    // 你可以從數(shù)據(jù)庫或文件加載授權(quán)規(guī)則
    // 但是你需要以 grbac.Rules 的格式返回你的身份驗證規(guī)則
    // 提示:你還可以將此函數(shù)綁定到golang結(jié)構(gòu)體
    return
}

func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // 在這里實現(xiàn)你的邏輯
    // ...
    // 這個邏輯可能是從請求的Headers中獲取token,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色扭仁。
    return roles, err
}

func Authorization() gin.HandlerFunc {
    // 在這里垮衷,我們通過“grbac.WithLoader”接口使用自定義Loader功能
    // 并指定應(yīng)每分鐘調(diào)用一次LoadAuthorizationRules函數(shù)以獲取最新的身份驗證規(guī)則。
    // Grbac還提供一些現(xiàn)成的Loader:
    // grbac.WithYAML
    // grbac.WithRules
    // grbac.WithJSON
    // ...
    rbac, err := grbac.New(grbac.WithLoader(LoadAuthorizationRules, time.Minute))
    if err != nil {
        panic(err)
    }
    return func(c *gin.Context) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        if err != nil {
            c.AbortWithError(http.StatusInternalServerError, err)
            return
        }
        state, _ := rbac.IsRequestGranted(c.Request, roles)
        if !state.IsGranted() {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    }
}

func main(){
    c := gin.New()
    c.Use(Authorization())

    // 在這里通過c.Get乖坠、c.Post等函數(shù)綁定你的API
    // ...

    c.Run(":8080")
}

2. 概念

這里有一些關(guān)于grbac的概念搀突。這很簡單,你可能只需要三分鐘就能理解熊泵。

2.1. Rule

// Rule即規(guī)則仰迁,用于定義Resource和Permission之間的關(guān)系
type Rule struct {
    // ID決定了Rule的優(yōu)先級。
    // ID值越大意味著Rule的優(yōu)先級越高顽分。
    // 當(dāng)請求被多個規(guī)則同時匹配時徐许,grbac將僅使用具有最高ID值的規(guī)則。
    // 如果有多個規(guī)則同時具有最大的ID卒蘸,則將隨機使用其中一個規(guī)則雌隅。
    ID int `json:"id"`
    *Resource
    *Permission
}

如你所見,Rule由三部分組成:ID悬秉,ResourcePermission澄步。
“ID”確定規(guī)則的優(yōu)先級。
當(dāng)請求同時滿足多個規(guī)則時(例如在通配符中)和泌,
grbac將選擇具有最高ID的那個村缸,然后使用其權(quán)限定義進行身份驗證。
如果有多個規(guī)則同時具有最大的ID武氓,則將隨機使用其中一個規(guī)則(所以請避免這種情況)梯皿。

下面有一個非常簡單的例子:

#Rule
- id: 0
  # Resource
  host: "*"
  path: "**"
  method: "*"
  # Permission
  authorized_roles:
  - "*"
  forbidden_roles: []
  allow_anyone: false

#Rule 
- id: 1
  # Resource
  host: domain.com
  path: "/article"
  method: "{DELETE,POST,PUT}"
  # Permission
  authorized_roles:
  - editor
  forbidden_roles: []
  allow_anyone: false

在以yaml格式編寫的此配置文件中仇箱,ID=0 的規(guī)則表明任何具有任何角色的人都可以訪問所有資源。
但是ID=1的規(guī)則表明只有editor可以對文章進行增刪改操作东羹。
這樣剂桥,除了文章的操作只能由editor訪問之外,任何具有任何角色的人都可以訪問所有其他資源属提。

2.2. Resource

type Resource struct {
    // Host 定義資源的Host权逗,允許使用增強的通配符。
    Host string `json:"host"`
    // Path 定義資源的Path冤议,允許使用增強的通配符斟薇。
    Path string `json:"path"`
    // Method 定義資源的Method,允許使用增強的通配符恕酸。
    Method string `json:"method"`
}

Resource用于描述Rule適用的資源堪滨。
當(dāng)執(zhí)行IsRequestGranted(c.Request,roles)時蕊温,grbac首先將當(dāng)前的Request與所有Rule中的Resources匹配袱箱。

Resource的每個字段都支持增強的通配符

2.3. Permission

// Permission用于定義權(quán)限控制信息
type Permission struct {
    // AuthorizedRoles定義允許訪問資源的角色
    // 支持的類型: 非空字符串,*
    //      *: 意味著任何角色义矛,但訪問者應(yīng)該至少有一個角色发笔,
    //      非空字符串:指定的角色
    AuthorizedRoles []string `json:"authorized_roles"`
    // ForbiddenRoles 定義不允許訪問指定資源的角色
    // ForbiddenRoles 優(yōu)先級高于AuthorizedRoles
    // 支持的類型:非空字符串,*
    //      *: 意味著任何角色症革,但訪問者應(yīng)該至少有一個角色筐咧,
    //      非空字符串:指定的角色
    //
    ForbiddenRoles []string `json:"forbidden_roles"`
    // AllowAnyone的優(yōu)先級高于 ForbiddenRoles、AuthorizedRoles
    // 如果設(shè)置為true噪矛,任何人都可以通過驗證量蕊。
    // 請注意,這將包括“沒有角色的人”艇挨。
    AllowAnyone bool `json:"allow_anyone"`
}

“Permission”用于定義綁定到的“Resource”的授權(quán)規(guī)則残炮。
這是易于理解的,當(dāng)請求者的角色符合“Permission”的定義時缩滨,他將被允許訪問Resource势就,否則他將被拒絕訪問。

為了加快驗證的速度脉漏,Permission中的字段不支持“增強的通配符”苞冯。
AuthorizedRolesForbiddenRoles中只允許*表示所有。

2.4. Loader

Loader用于加載Rule侧巨。 grbac預(yù)置了一些加載器舅锄,你也可以通過實現(xiàn)func()(grbac.Rules, error) 來自定義加載器并通過 grbac.WithLoader 加載它。

method description
WithJSON(path, interval) 定期從json文件加載規(guī)則配置
WithYaml(path, interval) 定期從yaml文件加載規(guī)則配置
WithRules(Rules) grbac.Rules加載規(guī)則配置
WithAdvancedRules(loader.AdvancedRules) 以一種更緊湊的方式定義Rule司忱,并使用loader.AdvancedRules加載
WithLoader(loader func()(Rules, error), interval) 使用自定義函數(shù)定期加載規(guī)則

interval定義了Rules的重載周期皇忿。
當(dāng)interval <0時畴蹭,grbac會放棄周期加載Rules配置;
當(dāng)interval∈[0,1s)時,grbac會自動將interval設(shè)置為5s;

3. 其他例子

這里有一些簡單的例子鳍烁,可以讓你更容易理解grbac的工作原理叨襟。
雖然grbac在大多數(shù)http框架中運行良好,但很抱歉我現(xiàn)在只使用gin幔荒,所以如果下面的例子中有一些缺陷糊闽,請告訴我。

3.1. gin && grbac.WithJSON

如果你想在JSON文件中編寫配置文件爹梁,你可以通過grbac.WithJSON(filepath墓怀,interval)加載它,filepath是你的json文件路徑卫键,并且grbac將每隔interval重新加載一次文件。 虱朵。

[
    {
        "id": 0,
        "host": "*",
        "path": "**",
        "method": "*",
        "authorized_roles": [
            "*"
        ],
        "forbidden_roles": [
            "black_user"
        ],
        "allow_anyone": false
    },
    {
        "id":1,
        "host": "domain.com",
        "path": "/article",
        "method": "{DELETE,POST,PUT}",
        "authorized_roles": ["editor"],
        "forbidden_roles": [],
        "allow_anyone": false
    }
]

以上是“JSON”格式的身份驗證規(guī)則示例莉炉。它的結(jié)構(gòu)基于grbac.Rules


func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // 在這里實現(xiàn)你的邏輯
    // ...
    // 這個邏輯可能是從請求的Headers中獲取token碴犬,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色絮宁。
    return roles, err
}

func Authentication() gin.HandlerFunc {
    rbac, err := grbac.New(grbac.WithJSON("config.json", time.Minute * 10))
    if err != nil {
        panic(err)
    }
    return func(c *gin.Context) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        if err != nil {
            c.AbortWithError(http.StatusInternalServerError, err)
            return
        }

        state, err := rbac.IsRequestGranted(c.Request, roles)
        if err != nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }

        if !state.IsGranted() {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
    }
}

func main(){
    c := gin.New()
    c.Use(Authentication())

    // 在這里通過c.Get、c.Post等函數(shù)綁定你的API
    // ...
    
    c.Run(":8080")
}

3.2. echo && grbac.WithYaml

如果你想在YAML文件中編寫配置文件服协,你可以通過grbac.WithYAML(file绍昂,interval)加載它,file是你的yaml文件路徑偿荷,并且grbac將每隔一個interval重新加載一次文件窘游。

#Rule
- id: 0
  # Resource
  host: "*"
  path: "**"
  method: "*"
  # Permission
  authorized_roles:
  - "*"
  forbidden_roles: []
  allow_anyone: false

#Rule 
- id: 1
  # Resource
  host: domain.com
  path: "/article"
  method: "{DELETE,POST,PUT}"
  # Permission
  authorized_roles:
  - editor
  forbidden_roles: []
  allow_anyone: false

以上是“YAML”格式的認證規(guī)則的示例。它的結(jié)構(gòu)基于grbac.Rules跳纳。

func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // 在這里實現(xiàn)你的邏輯
    // ...
    // 這個邏輯可能是從請求的Headers中獲取token忍饰,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色。
    return roles, err
}

func Authentication() echo.MiddlewareFunc {
    rbac, err := grbac.New(grbac.WithYAML("config.yaml", time.Minute * 10))
    if err != nil {
            panic(err)
    }
    return func(echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            roles, err := QueryRolesByHeaders(c.Request().Header)
            if err != nil {
                    c.NoContent(http.StatusInternalServerError)
                    return nil
            }
            state, err := rbac.IsRequestGranted(c.Request(), roles)
            if err != nil {
                    c.NoContent(http.StatusInternalServerError)
                    return nil
            }
            if state.IsGranted() {
                    return nil
            }
            c.NoContent(http.StatusUnauthorized)
            return nil
        }
    }
}

func main(){
    c := echo.New()
    c.Use(Authentication())

    // 在這里通過c.Get寺庄、c.Post等函數(shù)綁定你的API
    // ...
    
}

3.3. iris && grbac.WithRules

如果你想直接在代碼中編寫認證規(guī)則艾蓝,grbac.WithRules(rules)提供了這種方式,你可以像這樣使用它:


func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // 在這里實現(xiàn)你的邏輯
    // ...
    // 這個邏輯可能是從請求的Headers中獲取token斗塘,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色赢织。
    return roles, err
}

func Authentication() iris.Handler {
    var rules = grbac.Rules{
        {
            ID: 0,
            Resource: &grbac.Resource{
                        Host: "*",
                Path: "**",
                Method: "*",
            },
            Permission: &grbac.Permission{
                AuthorizedRoles: []string{"*"},
                ForbiddenRoles: []string{"black_user"},
                AllowAnyone: false,
            },
        },
        {
            ID: 1,
            Resource: &grbac.Resource{
                    Host: "domain.com",
                Path: "/article",
                Method: "{DELETE,POST,PUT}",
            },
            Permission: &grbac.Permission{
                    AuthorizedRoles: []string{"editor"},
                ForbiddenRoles: []string{},
                AllowAnyone: false,
            },
        },
    }
    rbac, err := grbac.New(grbac.WithRules(rules))
    if err != nil {
        panic(err)
    }
    return func(c context.Context) {
        roles, err := QueryRolesByHeaders(c.Request().Header)
        if err != nil {
                c.StatusCode(http.StatusInternalServerError)
            c.StopExecution()
            return
        }
        state, err := rbac.IsRequestGranted(c.Request(), roles)
        if err != nil {
                c.StatusCode(http.StatusInternalServerError)
            c.StopExecution()
            return
        }
        if !state.IsGranted() {
                c.StatusCode(http.StatusUnauthorized)
            c.StopExecution()
            return
        }
    }
}

func main(){
    c := iris.New()
    c.Use(Authentication())

    // 在這里通過c.Get、c.Post等函數(shù)綁定你的API
    // ...
    
}

3.4. ace && grbac.WithAdvancedRules

如果你想直接在代碼中編寫認證規(guī)則馍盟,grbac.WithAdvancedRules(rules)提供了這種方式于置,你可以像這樣使用它:


func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // 在這里實現(xiàn)你的邏輯
    // ...
    // 這個邏輯可能是從請求的Headers中獲取token,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色朽合。
    return roles, err
}

func Authentication() ace.HandlerFunc {
    var advancedRules = loader.AdvancedRules{
        {
            Host: []string{"*"},
            Path: []string{"**"},
            Method: []string{"*"},
            Permission: &grbac.Permission{
                AuthorizedRoles: []string{},
                ForbiddenRoles: []string{"black_user"},
                AllowAnyone: false,
            },
        },
        {
            Host: []string{"domain.com"},
            Path: []string{"/article"},
            Method: []string{"PUT","DELETE","POST"},
            Permission: &grbac.Permission{
                AuthorizedRoles: []string{"editor"},
                ForbiddenRoles: []string{},
                AllowAnyone: false,
            },
        },
    }
    auth, err := grbac.New(grbac.WithAdvancedRules(advancedRules))
    if err != nil {
        panic(err)
    }
    return func(c *ace.C) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        if err != nil {
        c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
        state, err := auth.IsRequestGranted(c.Request, roles)
        if err != nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
        if !state.IsGranted() {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    }
}

func main(){
    c := ace.New()
    c.Use(Authentication())

    // 在這里通過c.Get俱两、c.Post等函數(shù)綁定你的API
    // ...
    
}

loader.AdvancedRules試圖提供一種比grbac.Rules更緊湊的定義鑒權(quán)規(guī)則的方法饱狂。

3.5. gin && grbac.WithLoader


func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // 在這里實現(xiàn)你的邏輯
    // ...
    // 這個邏輯可能是從請求的Headers中獲取token,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色宪彩。
    return roles, err
}

type MySQLLoader struct {
    session *gorm.DB
}

func NewMySQLLoader(dsn string) (*MySQLLoader, error) {
    loader := &MySQLLoader{}
    db, err := gorm.Open("mysql", dsn)
    if err  != nil {
        return nil, err
    }
    loader.session = db
    return loader, nil
}

func (loader *MySQLLoader) LoadRules() (rules grbac.Rules, err error) {
    // 在這里實現(xiàn)你的邏輯
    // ...
    // 你可以從數(shù)據(jù)庫或文件加載授權(quán)規(guī)則
    // 但是你需要以 grbac.Rules 的格式返回你的身份驗證規(guī)則
    // 提示:你還可以將此函數(shù)綁定到golang結(jié)構(gòu)體
    return
}

func Authentication() gin.HandlerFunc {
    loader, err := NewMySQLLoader("user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        panic(err)
    }
    rbac, err := grbac.New(grbac.WithLoader(loader.LoadRules, time.Second * 5))
    if err != nil {
        panic(err)
    }
    return func(c *gin.Context) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        if err != nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
            
        state, err := rbac.IsRequestGranted(c.Request, roles)
        if err != nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
        if !state.IsGranted() {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    }
}

func main(){
    c := gin.New()
    c.Use(Authorization())

    // 在這里通過c.Get休讳、c.Post等函數(shù)綁定你的API
    // ...

    c.Run(":8080")
}

4. 增強的通配符

Wildcard支持的語法:

pattern:
  { term }
term:
  '*'         匹配任何非路徑分隔符的字符串
  '**'        匹配任何字符串,包括路徑分隔符.
  '?'         匹配任何單個非路徑分隔符
  '[' [ '^' ] { character-range } ']'
        character class (must be non-empty)
  '{' { term } [ ',' { term } ... ] '}'
  c           匹配字符 c (c != '*', '?', '\\', '[')
  '\\' c      匹配字符 c

character-range:
  c           匹配字符 c (c != '\\', '-', ']')
  '\\' c      匹配字符 c
  lo '-' hi   匹配字符 c for lo <= c <= hi

5. 運行效率

? gos test -bench=. 
goos: linux
goarch: amd64
pkg: github.com/storyicon/grbac/pkg/tree
BenchmarkTree_Query                   2000           541397 ns/op
BenchmarkTree_Foreach_Query           2000           1360719 ns/op
PASS
ok      github.com/storyicon/grbac/pkg/tree     13.182s

測試用例包含1000個隨機規(guī)則尿孔,“BenchmarkTree_Query”和“BenchmarkTree_Foreach_Query”函數(shù)分別測試四個請求:

541397/(4*1e9)=0.0001s

當(dāng)有1000條規(guī)則時俊柔,每個請求的平均驗證時間為“0.0001s”,這很快(大多數(shù)時間在通配符的匹配上)活合。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末雏婶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子白指,更是在濱河造成了極大的恐慌留晚,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件告嘲,死亡現(xiàn)場離奇詭異错维,居然都是意外死亡,警方通過查閱死者的電腦和手機橄唬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門赋焕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人仰楚,你說我怎么就攤上這事隆判。” “怎么了僧界?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵侨嘀,是天一觀的道長。 經(jīng)常有香客問我捎泻,道長飒炎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任笆豁,我火速辦了婚禮郎汪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闯狱。我一直安慰自己煞赢,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布哄孤。 她就那樣靜靜地躺著照筑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上凝危,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天波俄,我揣著相機與錄音,去河邊找鬼蛾默。 笑死懦铺,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的支鸡。 我是一名探鬼主播冬念,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牧挣!你這毒婦竟也來了急前?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瀑构,失蹤者是張志新(化名)和其女友劉穎裆针,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寺晌,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡据块,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了折剃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡像屋,死狀恐怖怕犁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情己莺,我是刑警寧澤奏甫,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站凌受,受9級特大地震影響阵子,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胜蛉,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一挠进、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧誊册,春花似錦领突、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春金砍,著一層夾襖步出監(jiān)牢的瞬間局蚀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工恕稠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留琅绅,地道東北人。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓谱俭,卻偏偏與公主長得像奉件,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子昆著,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359