Go語言 gin框架集成Casbin訪問權(quán)限控制

1. Casbin是什么?

Casbin是一個強(qiáng)大的、高效的開源訪問控制框架瓤的,其權(quán)限管理機(jī)制支持多種訪問控制模型圈膏。因此Casbin不能做身份驗證, 最佳的實踐是只負(fù)責(zé)訪問控制

1.1 Casbin的model

Casbin 中, 訪問控制模型被抽象為基于 PERM (Policy, Effect, Request, Matcher) 的一個文件,這個文件的具體呈現(xiàn)是一個以 .conf 作為后綴的文件

example :

rbac_model.conf

# Request定義
[request_definition]
r = sub, obj, act

# 策略定義
[policy_definition]
p = sub, obj, act

# 角色定義
[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

# 匹配器定義
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

對于上面配置文件簡單的理解是:


1.1.1 [request_definition]

r = sub, obj, act :定義請求由三部分組成 訪問用戶的用戶 Subject , 訪問的資源 Object 訪問的動作 Action

1.1.2 [policy_definition]

p = sub, obj, act : 定策略的格式 , 參數(shù)的基本意思和定義請求的相同 ,定義好了策略格式,那么對于策略(Policy)的具體描述可以存放在一個以 .csv 作為后綴的文件中

example :

rbac_Policy_example.csv

g, coder, root
g, zhangsan coder
p, root,api/v1/ping,GET
p, coder,api/v1/pong,GET
g, lisi, manager
p, manager, api/v1/user,POST

上面的rbac策略中我們定義了三條策略和三個用戶組,我們來看一下這些策略都有啥作用

  1. coder是root的角色
  2. zhangsan是coder的角色
  3. root 可以訪問 api/v1/ping 資源 通過GET動作,那么coder , zhangsan也可以訪問
  4. coder可以訪問 api/v1/pong 資源 通過GET動作,zhangsan也能訪問
  5. lisi是manager的角色
  6. manager可以訪問 api/v1/user資源通過POST動作,lisi也可以訪問
1.1.3 [role_definition]

**g = _, _ ** : 是RBAC角色繼承關(guān)系的定義 ,此處的 _, _ 表示 前項繼承后項角色的權(quán)限

1.1.4 [policy_effect]

e = some(where (p.eft == allow)) : 表示任意一條Policy策略滿足那么結(jié)果就為allow

1.1.5 [matchers]

m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act : 定義了策略匹配者睦擂。匹配者是一組表達(dá)式。它定義了如何根據(jù)請求來匹配策略規(guī)則,匹配表達(dá)式的寫法比較靈活根據(jù)具體需求來編寫即可.
而此處的表達(dá)式意思是 ,檢測用戶角色 && 檢測用戶訪問的資源 &&檢測用戶的動作 (&&表示并且關(guān)系,當(dāng)然也有其他邏輯運算符 ||,!等)

1.2 Casbin的Policy

Policy 主要表示訪問控制關(guān)于角色,資源,行為的具體映射關(guān)系這比較好處理,但是這種映射關(guān)系怎么存儲就值得考慮了

1.2.1 csv 文件存儲
訪問控制模型 Model 文件 Policy 文件
ACL basic_model.conf basic_policy.csv
具有超級用戶的ACL basic_with_root_model.conf basic_policy.csv
沒有用戶的ACL basic_without_users_model.conf basic_without_users_policy.csv
沒有資源的ACL basic_without_resources_model.conf basic_without_resources_policy.csv
RBAC rbac_model.conf rbac_policy.csv
支持資源角色的RBAC rbac_with_resource_roles_model.conf rbac_with_resource_roles_policy.csv
支持域/租戶的RBAC rbac_with_domains_model.conf rbac_with_domains_policy.csv
ABAC abac_model.conf
RESTful keymatch_model.conf keymatch_policy.csv
拒絕優(yōu)先 rbac_with_not_deny_model.conf rbac_with_deny_policy.csv
Allow-and-deny rbac_with_deny_model.conf rbac_with_deny_policy.csv
Priority priority_model.conf priority_policy.csv
1.2.2 適配器存儲

casbin的適配器 adapter 可以從存儲中加載策略規(guī)則,也可將策略規(guī)則保存到不同的存儲系統(tǒng)中

支持如: MySQL, PostgreSQL, SQL Server, SQLite3,MongoDB,Redis,Cassandra DB等等存儲系統(tǒng)

適配器 類型 作者 自動保存 描述
File Adapter (內(nèi)置) File Casbin ? For .CSV (Comma-Separated Values) files
Filtered File Adapter (內(nèi)置) File @faceless-saint ? For .CSV (Comma-Separated Values) files with policy subset loading support
SQL Adapter SQL @Blank-Xu ? MySQL, PostgreSQL, SQL Server, SQLite3 are supported in master branch and Oracle is supported in oracle branch by database/sql
Xorm Adapter ORM Casbin ? MySQL, PostgreSQL, TiDB, SQLite, SQL Server, Oracle are supported by Xorm
Gorm Adapter ORM Casbin ? MySQL, PostgreSQL, Sqlite3, SQL Server are supported by Gorm
Beego ORM Adapter ORM Casbin ? MySQL, PostgreSQL, Sqlite3 are supported by Beego ORM
SQLX Adapter ORM @memwey ? MySQL, PostgreSQL, SQLite, Oracle are supported by SQLX
Sqlx Adapter SQL @Blank-Xu ? MySQL, PostgreSQL, SQL Server, SQLite3 are supported in master branch and Oracle is supported in oracle branch by sqlx
GF ORM Adapter ORM @vance-liu ? MySQL, SQLite, PostgreSQL, Oracle, SQL Server are supported by GF ORM
Filtered PostgreSQL Adapter SQL Casbin ? For PostgreSQL
PostgreSQL Adapter SQL @cychiuae ? For PostgreSQL
PostgreSQL Adapter (Archived) SQL Going ? For PostgreSQL
RQLite Adapter SQL EDOMO Systems ? For RQLite
MongoDB Adapter NoSQL Casbin ? For MongoDB based on MongoDB driver for Go
MongoDB Adapter NoSQL Titan DC ? For MongoDB based on MongoDB Go driver
RethinkDB Adapter NoSQL @adityapandey9 ? For RethinkDB
Cassandra Adapter NoSQL Casbin ? For Apache Cassandra DB
DynamoDB Adapter NoSQL HOOQ ? For Amazon DynamoDB
Dynacasbin NoSQL NewbMiao ? For Amazon DynamoDB
ArangoDB Adapter NoSQL @adamwasila ? For ArangoDB
Amazon S3 Adapter Cloud Soluto ? For Minio and Amazon S3
Azure Cosmos DB Adapter Cloud @spacycoder ? For Microsoft Azure Cosmos DB
GCP Datastore Adapter Cloud LivingPackets ? For Google Cloud Platform Datastore
GCP Firestore Adapter Cloud @reedom ? For Google Cloud Platform Firestore
Consul Adapter KV store @ankitm123 ? For HashiCorp Consul
Redis Adapter KV store Casbin ? For Redis
Etcd Adapter KV store @sebastianliu ? For etcd
BoltDB Adapter KV store @speza ? For Bolt
Bolt Adapter KV store @wirepair ? For Bolt
BadgerDB Adapter KV store @inits ? For BadgerDB
Protobuf Adapter Stream Casbin ? For Google Protocol Buffers
JSON Adapter String Casbin ? For JSON
String Adapter String @qiangmzsx ? For String

2. gin集成Casbin實現(xiàn)RESTful接口訪問控制

2.1 go mod 構(gòu)建項目

# 新建個叫做ginCasbin的gomod項目(項目名自定義)
go mod init GinCasbin

2.2 安裝依賴包

# 安裝依賴包
# 安裝gin框架
go get -u github.com/gin-gonic/gin
# Go語言casbin的依賴包
go get github.com/casbin/casbin
# gorm 適配器依賴包
go get github.com/casbin/gorm-adapter
# mysql驅(qū)動依賴
go get github.com/go-sql-driver/mysql
# gorm 包
go get github.com/jinzhu/gorm
# 高性能緩存BigCache
go get github.com/allegro/bigcache/v2

2.3 目錄規(guī)劃說明

├─app # 業(yè)務(wù)目錄
│  ├─api  ## 存放api的目錄(暫時不用)
│  ├─model ## 存放實體的目錄(暫時不用)
│  └─service ## 存放業(yè)務(wù)代碼的目錄(暫時不用)
├─config # 存放配置文件的目錄
├─middleware # 存放中間件的目錄
├─routers # 存放路由的目錄
└─utils # 常用工具組件目錄
    ├─ACS ## 存放訪問控制執(zhí)行器目錄
    ├─APIResponse ##  存放API統(tǒng)一響應(yīng)函數(shù)目錄
    ├─Cache ## 緩存工具目錄
    └─DB ## 數(shù)據(jù)連接文件目錄
├─go.mod
├─go.sum
├─main.go # 項目入口文件

2.4 項目代碼開發(fā)

2.4.1 工具組件開發(fā)
# 進(jìn)入utils目錄
cd utils

DB/mysql.go

package DB

import (
    "fmt"
    "github.com/jinzhu/gorm"
)
import _ "github.com/go-sql-driver/mysql"

var (
    Mysql *gorm.DB
)

func init() {
    var err error
    dsn := "root:root@(127.0.0.1:3306)/xz_boss?charset=utf8&parseTime=True&loc=Local"
    Mysql, err = gorm.Open("mysql", dsn)
    if err != nil {
        fmt.Println("connect DB error")
        panic(err)
    }
}

ACS/enforcer.go

package ACS

import (
    "GinCasbin/utils/DB"
    "github.com/casbin/casbin"
    "github.com/casbin/gorm-adapter"
)

var Enforcer *casbin.Enforcer

func init() {
    // mysql 適配器
    adapter := gormadapter.NewAdapterByDB(DB.Mysql)
    // 通過mysql適配器新建一個enforcer
    Enforcer = casbin.NewEnforcer("config/keymatch2_model.conf", adapter)
    // 日志記錄
    Enforcer.EnableLog(true)
}


APIResponse/response.go

package APIResponse

import "github.com/gin-gonic/gin"

type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}

var C *gin.Context

func Error(message string) {
    if len(message) == 0 {
        message = "fail"
    }
    C.JSON(200, Response{
        Code:    -1,
        Message: message,
        Data:    nil,
    })
}
func Success(data interface{}) {
    C.JSON(200, Response{
        Code:    200,
        Message: "success",
        Data:    data,
    })
}

Cache/big.go

package Cache

import (
    "github.com/allegro/bigcache/v2"
    "time"
)

var GlobalCache *bigcache.BigCache

func init() {
    // 初始化BigCache實例
    GlobalCache, _ = bigcache.NewBigCache(bigcache.DefaultConfig(30 * time.Minute))
}

2.4.2 配置文件

常規(guī)項目中配置文件目錄中會存放各種配置文件,在這個Demo中僅將casbin的模型文件放在這里

cd ../config

config/keymatch2_model.conf

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
2.4.3 中間件

此處我們編寫的一個基于casbin權(quán)限控制的中間件

cd ../middleware

middleware/privilege.go

package middleware

import (
    "GinCasbin/utils/ACS"
    "GinCasbin/utils/APIResponse"
    "GinCasbin/utils/Cache"
    "github.com/gin-gonic/gin"
    "log"
)

func Privilege() gin.HandlerFunc {
    return func(c *gin.Context) {
        APIResponse.C = c
        var userName = c.GetHeader("userName")
        if userName == "" {
            APIResponse.Error("header miss userName")
            c.Abort()
            return
        }
        path := c.Request.URL.Path
        method := c.Request.Method
        cacheName := userName + path + method
        // 從緩存中讀取&判斷
        entry, err := Cache.GlobalCache.Get(cacheName)
        if err == nil && entry != nil {
            if string(entry) == "true" {
                c.Next()
            } else {
                APIResponse.Error("access denied")
                c.Abort()
                return
            }
        } else {
            // 從數(shù)據(jù)庫中讀取&判斷
            //記錄日志
            ACS.Enforcer.EnableLog(true)
            // 加載策略規(guī)則
            err := ACS.Enforcer.LoadPolicy()
            if err != nil {
                log.Println("loadPolicy error")
                panic(err)
            }
            // 驗證策略規(guī)則
            result, err := ACS.Enforcer.EnforceSafe(userName, path, method)
            if err != nil {
                APIResponse.Error("No permission found")
                c.Abort()
                return
            }
            if !result {
                // 添加到緩存中
                Cache.GlobalCache.Set(cacheName, []byte("false"))
                APIResponse.Error("access denied")
                c.Abort()
                return
            } else {
                Cache.GlobalCache.Set(cacheName, []byte("true"))
            }
            c.Next()
        }
    }
}

2.4.4 路由文件
cd ../routers

routers/route.go

package routers

import (
    "GinCasbin/middleware"
    "GinCasbin/utils/ACS"
    "GinCasbin/utils/APIResponse"
    "GinCasbin/utils/Cache"
    "github.com/gin-gonic/gin"
)

var (
    R *gin.Engine
)

func init() {
    R = gin.Default()
    R.NoRoute(func(c *gin.Context) {
        c.JSON(400, gin.H{"code": 400, "message": "Bad Request"})
    })
    api()
}
func api() {
    auth := R.Group("/api")
    {
        // 模擬添加一條Policy策略
        auth.POST("acs", func(c *gin.Context) {
            APIResponse.C = c
            subject := "tom"
            object := "/api/routers"
            action := "POST"
            cacheName := subject + object + action
            result := ACS.Enforcer.AddPolicy(subject, object, action)
            if result {
                // 清除緩存
                _ = Cache.GlobalCache.Delete(cacheName)
                APIResponse.Success("add success")
            } else {
                APIResponse.Error("add fail")
            }
        })
        // 模擬刪除一條Policy策略
        auth.DELETE("acs/:id", func(context *gin.Context) {
            APIResponse.C = context
            result := ACS.Enforcer.RemovePolicy("tom", "/api/routers", "POST")
            if result {
                // 清除緩存 代碼省略
                APIResponse.Success("delete Policy success")
            } else {
                APIResponse.Error("delete Policy fail")
            }
        })
        // 獲取路由列表
        auth.POST("/routers", middleware.Privilege(), func(c *gin.Context) {
            type data struct {
                Method string `json:"method"`
                Path   string `json:"path"`
            }
            var datas []data
            routers := R.Routes()
            for _, v := range routers {
                var temp data
                temp.Method = v.Method
                temp.Path = v.Path
                datas = append(datas, temp)
            }
            APIResponse.C = c
            APIResponse.Success(datas)
            return
        })
    }
    // 定義路由組
    user := R.Group("/api/v1")
    // 使用訪問控制中間件
    user.Use(middleware.Privilege())
    {
        user.POST("user", func(c *gin.Context) {
            c.JSON(200, gin.H{"code": 200, "message": "user add success"})
        })
        user.DELETE("user/:id", func(c *gin.Context) {
            id := c.Param("id")
            c.JSON(200, gin.H{"code": 200, "message": "user delete success " + id})
        })
        user.PUT("user/:id", func(c *gin.Context) {
            id := c.Param("id")
            c.JSON(200, gin.H{"code": 200, "message": "user update success " + id})
        })
        user.GET("user/:id", func(c *gin.Context) {
            id := c.Param("id")
            c.JSON(200, gin.H{"code": 200, "message": "user Get success " + id})
        })
    }
}

2.4.5 項目入口文件
cd ..

main.go

package main

import (
    . "GinCasbin/routers"
)

func main() {
    R.Run()
}

2.5 測試訪問策略

2.5.1 啟動項目
# 運行項目
go run main.go
# gin框架在debug模式下的輸出
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /api/acs                  --> GinCasbin/routers.api.func1 (3 handlers)
[GIN-debug] DELETE /api/acs/:id              --> GinCasbin/routers.api.func2 (3 handlers)
[GIN-debug] POST   /api/routers              --> GinCasbin/routers.api.func3 (4 handlers)
[GIN-debug] POST   /api/v1/user              --> GinCasbin/routers.api.func4 (4 handlers)
[GIN-debug] DELETE /api/v1/user/:id          --> GinCasbin/routers.api.func5 (4 handlers)
[GIN-debug] PUT    /api/v1/user/:id          --> GinCasbin/routers.api.func6 (4 handlers)
[GIN-debug] GET    /api/v1/user/:id          --> GinCasbin/routers.api.func7 (4 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

2.5.2 測試casbin訪問控制

新開啟一個命令行終端

# 訪問接口
# 參數(shù)缺失
curl -X POST http://127.0.0.1:8080/api/routers
{"code":-1,"message":"header miss userName","data":null}


# 無訪問權(quán)限
curl -X POST -H "userName:tom" http://127.0.0.1:8080/api/routers
{"code":-1,"message":"access denied","data":null}


# 添加一條規(guī)則(代碼中是模擬數(shù)據(jù))
curl -X POST http://127.0.0.1:8080/api/acs
{"code":200,"message":"success","data":"add success"}

# 再次訪問(有訪問權(quán)限,可以訪問)
curl -X POST -H "userName:tom" http://127.0.0.1:8080/api/routers
{
    "code":200,
    "message":"success",
    "data":[
        {
            "method":"POST",
            "path":"/api/acs"
        },
        {
            "method":"POST",
            "path":"/api/routers"
        },
        {
            "method":"POST",
            "path":"/api/v1/user"
        },
        {
            "method":"DELETE",
            "path":"/api/acs/:id"
        },
        {
            "method":"DELETE",
            "path":"/api/v1/user/:id"
        },
        {
            "method":"PUT",
            "path":"/api/v1/user/:id"
        },
        {
            "method":"GET",
            "path":"/api/v1/user/:id"
        }
    ]
}

# 直接向數(shù)據(jù)庫添加幾條Policy策略
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user', 'POST', NULL, NULL, NULL);
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user/:id', 'GET', NULL, NULL, NULL);
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user/:id', 'PUT', NULL, NULL, NULL);

#再測試
## 添加接口
curl -X POST -H "userName:admin" http://127.0.0.1:8080/api/v1/user
{"code":200,"message":"user add success"}
## 查詢接口
curl -X GET -H "userName:admin" http://127.0.0.1:8080/api/v1/user/99
{"code":200,"message":"user Get success 99"}
## 更新接口
curl -X PUT -H "userName:admin" http://127.0.0.1:8080/api/v1/user/199
{"code":200,"message":"user update success 199"}
## 刪除接口(沒有分配訪問權(quán)限)
curl -X DELETE -H "userName:admin" http://127.0.0.1:8080/api/v1/user/299
{"code":-1,"message":"access denied","data":null}

2.6 其他

casbin的一些適配器有自動保存功能而另外一些則沒有,有自動保存功能的適配器會在連接數(shù)據(jù)的時候自動創(chuàng)建一張表用來保存Policy策略數(shù)據(jù)(替代存儲Policy的csv文件)

上述 Demo 的SQL文件如下(該表是gorm適配器自動創(chuàng)建的)

casbin_rule.sql

-- ----------------------------
-- Table structure for casbin_rule
-- ----------------------------
DROP TABLE IF EXISTS `casbin_rule`;
CREATE TABLE `casbin_rule` (
  `p_type` varchar(100) DEFAULT NULL,
  `v0` varchar(100) DEFAULT NULL,
  `v1` varchar(100) DEFAULT NULL,
  `v2` varchar(100) DEFAULT NULL,
  `v3` varchar(100) DEFAULT NULL,
  `v4` varchar(100) DEFAULT NULL,
  `v5` varchar(100) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of casbin_rule
-- ----------------------------
INSERT INTO `casbin_rule` VALUES ('p', 'zhangsan', '/api/v1/ping', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'coder', '/api/v2/user/:id', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'coder', '/api/v2/routers', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user', 'POST', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user/:id', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user/:id', 'PUT', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'tom', '/api/routers', 'POST', '', '', '');

參考資料

- [1] casbin

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末斟赚,一起剝皮案震驚了整個濱河市差油,隨后出現(xiàn)的幾起案子拗军,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件发侵,死亡現(xiàn)場離奇詭異侈咕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)器紧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門耀销,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人铲汪,你說我怎么就攤上這事〈咧玻” “怎么了省核?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵旧噪,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮怪瓶,結(jié)果婚禮上敛滋,老公的妹妹穿的比我還像新娘杂曲。我一直安慰自己,他們只是感情好述召,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布夺刑。 她就那樣靜靜地躺著耘斩,像睡著了一般岩饼。 火紅的嫁衣襯著肌膚如雪寞冯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天胰默,我揣著相機(jī)與錄音奴迅,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼之众,長吁一口氣:“原來是場噩夢啊……” “哼膘婶!你這毒婦竟也來了脊岳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤莫瞬,失蹤者是張志新(化名)和其女友劉穎檩小,沒想到半個月后瓦戚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啡捶,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡玄糟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年箱沦,在試婚紗的時候發(fā)現(xiàn)自己被綠了聘萨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡址遇,死狀恐怖跺株,靈堂內(nèi)的尸體忽然破棺而出砸泛,到底是詐尸還是另有隱情围俘,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布圈匆,位于F島的核電站砚作,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏继低。R本人自食惡果不足惜柄驻,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧疟位,春花似錦章贞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凝垛。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間福扬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留冶伞,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像顷级,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子控硼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345