用 Go 写一个轻量级的 ldap 测试工具

image.png

前言

这是一个轮子。

作为一个在高校里混的 IT,LDAP 我们其实都蛮熟悉的,因为在高校中使用 LDAP 来做统一认证还蛮普遍的。对于 LDAP 的管理员而言,LDAP 的各种操作自然有产品对应的管理工具来处理,但对于需要集成 LDAP 的用户而言,我们经常需要做一些 LDAP 的测试来作为集成时的对比验证,脑补以下场景:

系统调试ing
乙:“LDAP 认证走不通啊,你们的 LDAP 是不是有问题哦”
默默掏出测试工具
甲:“你看,毫无压力”
乙:“我再查查看~”

另外,高校间协作共享会比较多一些,例如通过一些联邦式的认证联盟来让联盟内的成员互相信任身份认证的结果,从而支持一些跨校协作的应用。在国外应用的比较多的是基于 Shibboleth 的联盟。国内在上海有一个基于相同技术框架的联盟,称之为上海市教育认证联盟。

image.png

我校作为上海联盟的主要技术支持方,我经常得和各个学校的 LDAP 打交道。远程支持当然只有 ssh 了。此时要测试 LDAP,LdapBrowser 之类的工具在纯 CLI 环境下没法用,openldap 的 client 又显得过于麻烦,所以就造个轮子咯。

需求

这个轮子需求大概是这个样子

  1. 跨平台,木有依赖,开箱即用。用 Go 来撸一个就能很好的满足这个需求。
  2. 简单无脑一点,搞复杂了就没意思了
  3. 做到 ldap 的认证和查询就够了。增删改涉及 schema 以及不同 LDAP 产品实现时的标准差异,要做到兼容通用会比较麻烦。反正这一块的需求管理员用产品自带的控制台就好了嘛,我们的测试工具的就不折腾了
  4. 支持批量查询和批量认证的测试
  5. 提供个简单的 HTTP API,必要时也可以提供基于 http 的远程测试。
  6. 好吧,还可以学习 Golang ~

用 Go 操作 LDAP

我们可以用 https://github.com/go-ldap/ldap 这个库来操作 LDAP
他的 example 给的非常的详细,基本看一遍就可以开始抄了。。。

我们拿其中 userAuthentication 的 example 来举个例子,下为 example 中的示例代码,我增加了若干注释说明

func Example_userAuthentication() {
    // The username and password we want to check
    // 用来认证的用户名和密码
    username := "someuser"
    password := "userpassword"

    // 用来获取查询权限的 bind 用户。如果 ldap 禁止了匿名查询,那我们就需要先用这个帐户 bind 以下才能开始查询
    // bind 的账号通常要使用完整的 DN 信息。例如 cn=manager,dc=example,dc=org
    // 在 AD 上,则可以用诸如 mananger@example.org 的方式来 bind
    bindusername := "readonly"
    bindpassword := "password"

    l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))
    if err != nil {
        log.Fatal(err)
    }
    defer l.Close()

    // Reconnect with TLS
    // 建立 StartTLS 连接,这是建立纯文本上的 TLS 协议,允许你将非加密的通讯升级为 TLS 加密而不需要另外使用一个新的端口。
    // 邮件的 POP3 ,IMAP 也有支持类似的 StartTLS,这些都是有 RFC 的
    err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
    if err != nil {
        log.Fatal(err)
    }

    // First bind with a read only user
    // 先用我们的 bind 账号给 bind 上去
    err = l.Bind(bindusername, bindpassword)
    if err != nil {
        log.Fatal(err)
    }

    // Search for the given username
    // 这样我们就有查询权限了,可以构造查询请求了
    searchRequest := ldap.NewSearchRequest(
        // 这里是 basedn,我们将从这个节点开始搜索
        "dc=example,dc=com",
        // 这里几个参数分别是 scope, derefAliases, sizeLimit, timeLimit,  typesOnly
        // 详情可以参考 RFC4511 中的定义,文末有链接
        ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 
        // 这里是 LDAP 查询的 Filter。这个例子例子,我们通过查询 uid=username 且 objectClass=organizationalPerson。
        // username 即我们需要认证的用户名
        fmt.Sprintf("(&(objectClass=organizationalPerson)(uid=%s))", username),
        // 这里是查询返回的属性,以数组形式提供。如果为空则会返回所有的属性
        []string{"dn"},
        nil,
    )
    // 好了现在可以搜索了,返回的是一个数组
    sr, err := l.Search(searchRequest)
    if err != nil {
        log.Fatal(err)
    }

    // 如果没有数据返回或者超过1条数据返回,这对于用户认证而言都是不允许的。
    // 前这意味着没有查到用户,后者意味着存在重复数据
    if len(sr.Entries) != 1 {
        log.Fatal("User does not exist or too many entries returned")
    }

    // 如果没有意外,那么我们就可以获取用户的实际 DN 了
    userdn := sr.Entries[0].DN

    // Bind as the user to verify their password
    // 拿这个 dn 和他的密码去做 bind 验证
    err = l.Bind(userdn, password)
    if err != nil {
        log.Fatal(err)
    }

    // Rebind as the read only user for any further queries
    // 如果后续还需要做其他操作,那么使用最初的 bind 账号重新 bind 回来。恢复初始权限。
    err = l.Bind(bindusername, bindpassword)
    if err != nil {
        log.Fatal(err)
    }
}

总结:

  1. 建立连接
  2. 使用 bind 用户先 bind 以获取权限
  3. 根据用户名对应的属性写 searchfilter,结合 basedn 进行查询
  4. 如果需要认证,用查到的 dn 进行 bind 验证
  5. 如果还要继续查询/认证,rebind 回初始的 bind 用户上
  6. 关闭连接

命令行

作为一个 cli 工具,命令行部分的设计是很重要的。考虑我们所需要实现的功能

  • 用户查询
  • 用户认证
  • 用特定的 filter 查询
  • 批量认证
  • 批量查询

比如可以按这个方式进行罗列

image.png

Go 由一个非常好的 cli 库 cobra,我们就用它来做轮子。

cobra 用起来容易上手,我同样贴一段他的 example 代码来加以注释来说明

package main

import (
  "fmt"
  "strings"

  "github.com/spf13/cobra"
)

func main() {
  // 给后面的 Flags 用的
  var echoTimes int

  // cobra 以层次的方式组织命令。从 rootCmd 开始,每一个命令都通过一个 struct 来配置命令的相关信息
  // 这一行本来在 example 的最下面,我给挪上来了
  var rootCmd = &cobra.Command{Use: "app"}

  // 不同于 rootCmd,我们开始给出比较详细的配置了
  var cmdPrint = &cobra.Command{
  // 命令的名称,同时 [string to print] 等会在 help 时作为 usage 的内容输出
    Use:   "print [string to print]",
  // help 时作为 Available Commands 中,cmd 后的短描述
    Short: "Print anything to the screen",
  // help 时作为 cmd 的长描述
    Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
  // 限制命令最小参数输入为1,还有其他的参数限制,详见 github 上的说明
    Args: cobra.MinimumNArgs(1),
  // 命令执行的函数,把命令要干的事情放在这里就好了
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdEcho = &cobra.Command{
    Use:   "echo [string to echo]",
    Short: "Echo anything to the screen",
    Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdTimes = &cobra.Command{
    Use:   "times [# times] [string to echo]",
    Short: "Echo anything to the screen more times",
    Long: `echo things multiple times back to the user by providing
a count and a string.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      for i := 0; i < echoTimes; i++ {
        fmt.Println("Echo: " + strings.Join(args, " "))
      }
    },
  }

  // 这里为 cmdTimes 对应命令设置了一个 Flag 参数
  // 类型为 Int,输入方式为 `--times` 或者 `-t`,默认值时 1,绑定到最开始声明的 `echoTimes` 上。
  cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")

  // rootCmd 后面 Add 了 cmdPrint, cmdEcho
  // 也就是说初始的两个命令是 `print` 和 `echo`
  rootCmd.AddCommand(cmdPrint, cmdEcho)
  // cmdEcho 后面 Add 了 cmdTimes
  // 所以 `echo` 后面还有一个命令时 `times`
  cmdEcho.AddCommand(cmdTimes)
  rootCmd.Execute()
}

实际生产环境中,我们可以每个命令的相关代码单独放在一个 .go 文件中,这样看起来会比较清晰一些。像这样

├── cmd
│   ├── auth.go
│   ├── http.go
│   ├── root.go
│   ├── search.go
│   ├── utils.go
│   └── version.go
├── main.go

API

API 可以用著名的 beego 框架来搞。
beego 的文档 非常详细,就不再赘述了。

基于 beego ,我们提供以下 API,把命令行支持的功能都搬过来。

GET /api/v1/ldap/health
ldap 健康状态监测。请求的时候就去尝试连接一下 ldap,用 bind 账号 bind 测试下。成功的话就返回 ok,否则给个错。 
GET /api/v1/ldap/search/filter/:filter
根据 ldap filter 来做查询
GET /api/v1/ldap/search/user/:username
根据用户名来查询
POST /api/v1/ldap/search/multi
根据用户名同时查询多个用户,以 application/json 方式发送请求数据,例:
["user1","user2","user3"]
POST /api/v1/ldap/auth/single
单个用户的认证测试,以 application/json 方式发送请求数据,例:
{
    "username": "user",
    "password": "123456"
}
POST /api/v1/ldap/auth/multi
单个用户的认证测试,以 application/json 方式发送请求数据,例:
[{
    "username": "user1",
    "password": "123456"
}, {
    "username": "user2",
    "password": "654321"
}]

轮子

那么这个轮子已经造好了。ldao-test-tool

代码结构

# tree
.
├── cfg.json.example
├── cmd
│   ├── auth.go
│   ├── http.go
│   ├── root.go
│   ├── search.go
│   ├── utils.go
│   └── version.go
├── g
│   ├── cfg.go
│   └── const.go
├── http
│   ├── controllers
│   │   ├── authMulti.go
│   │   ├── authSingle.go
│   │   ├── default.go
│   │   ├── health.go
│   │   ├── searchFilter.go
│   │   ├── searchMulti.go
│   │   └── searchUser.go
│   ├── http.go
│   └── router.go
├── LICENSE
├── main.go
├── models
│   ├── funcs.go
│   ├── ldap.go
│   └── ldap_test.go
└── README.MD

编译

go get ./...
go build

release

可以直接下载编译好的 release 版本

提供 win64 和 linux64 两个平台的可执行文件

https://github.com/shanghai-edu/ldap-test-tool/releases/

配置文件

默认配置文件为目录下的 cfg.json,也可以使用 -c--config 来加载自定义的配置文件。

openldap 配置示例

{
    "ldap": {
        "addr": "ldap.example.org:389",
        "baseDn": "dc=example,dc=org",
        "bindDn": "cn=manager,dc=example,dc=org",
        "bindPass": "password",
        "authFilter": "(&(uid=%s))",
        "attributes": ["uid", "cn", "mail"],
        "tls":        false,
        "startTLS":   false
    },
    "http": {
        "listen": "0.0.0.0:8888"
    }
}

AD 配置示例

{
    "ldap": {
        "addr": "ad.example.org:389",
        "baseDn": "dc=example,dc=org",
        "bindDn": "manager@example.org",
        "bindPass": "password",
        "authFilter": "(&(sAMAccountName=%s))",
        "attributes": ["sAMAccountName", "displayName", "mail"],
        "tls":        false,
        "startTLS":   false
    },
    "http": {
        "listen": "0.0.0.0:8888"
    }
}

命令体系

命令行部分使用 cobra 框架,可以使用 help 命令查看命令的使用方式

# ./ldap-test-tool help
ldap-test-tool is a simple tool for ldap test
build by shanghai-edu.
Complete documentation is available at github.com/shanghai-edu/ldap-test-tool

Usage:
  ldap-test-tool [flags]
  ldap-test-tool [command]

Available Commands:
  auth        Auth Test
  help        Help about any command
  http        Enable a http server for ldap-test-tool
  search      Search Test
  version     Print the version number of ldap-test-tool

Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")
  -h, --help            help for ldap-test-tool

Use "ldap-test-tool [command] --help" for more information about a command.

认证

./ldap-test-tool auth -h
Auth Test

Usage:
  ldap-test-tool auth [flags]
  ldap-test-tool auth [command]

Available Commands:
  multi       Multi Auth Test
  single      Single Auth Test

Flags:
  -h, --help   help for auth

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

Use "ldap-test-tool auth [command] --help" for more information about a command.
单用户测试

命令行说明

Single Auth Test

Usage:
  ldap-test-tool auth single [username] [password] [flags]

Flags:
  -h, --help   help for single

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

示例

./ldap-test-tool auth single qfeng 123456
LDAP Auth Start 
==================================

qfeng auth test successed 

==================================
LDAP Auth Finished, Time Usage 47.821884ms 
批量测试

命令行说明

# ./ldap-test-tool auth multi -h
Multi Auth Test

Usage:
  ldap-test-tool auth multi [filename] [flags]

Flags:
  -h, --help   help for multi

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

示例

# cat authusers.txt 
qfeng,123456
qfengtest,111111

用户名和密码以逗号分隔(csv风格)
authusers.txt 中有两个用户,密码正确的 qfeng 和密码错误的 qfengtest

# ./ldap-test-tool auth multi authusers.txt 
LDAP Multi Auth Start 
==================================

Successed count 1 
Failed count 1 
Failed users:
 -- User: qfengtest , Msg: Cannot find such user 

==================================
LDAP Multi Auth Finished, Time Usage 49.582994ms 

查询

# ./ldap-test-tool search -h
Search Test

Usage:
  ldap-test-tool search [flags]
  ldap-test-tool search [command]

Available Commands:
  filter      Search By Filter
  multi       Search Multi Users
  user        Search Single User

Flags:
  -h, --help   help for search

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

Use "ldap-test-tool search [command] --help" for more information about a command.
[root@wiki-qfeng ldap-test-tool]# 
单用户查询

命令行说明

# ./ldap-test-tool search user -h
Search Single User

Usage:
  ldap-test-tool search user [username] [flags]

Flags:
  -h, --help   help for user

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")
[root@wiki-qfeng ldap-test-tool]# 

示例

# ./ldap-test-tool search user qfeng
LDAP Search Start 
==================================


DN: uid=qfeng,ou=people,dc=example,dc=org
Attributes:
 -- uid  : qfeng 
 -- cn   : 冯骐测试 
 -- mail : qfeng@example.org


==================================
LDAP Search Finished, Time Usage 44.711268ms 

PS: 如果属性有多值,将以 ; 分割

LDAP Filter 查询
# ./ldap-test-tool search filter -h
Search By Filter

Usage:
  ldap-test-tool search filter [searchFilter] [flags]

Flags:
  -h, --help   help for filter

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

示例

# ./ldap-test-tool search filter "(cn=*测试)"
LDAP Search By Filter Start 
==================================


DN: uid=test1,ou=people,dc=example,dc=org
Attributes:
 -- uid  : test1 
 -- cn   : 一号测试 
 -- mail : test1@example.org 


DN: uid=test2,ou=people,dc=example,dc=org
Attributes:
 -- uid  : test2 
 -- cn   : 二号测试 
 -- mail : test2@example.org 


DN: uid=test3,ou=people,dc=example,dc=org
Attributes:
 -- uid  : test3
 -- cn   : 三号测试 
 -- mail : test3@example.org 

results count  3

==================================
LDAP Search By Filter Finished, Time Usage 46.071833ms 
批量查询测试

命令行说明

# ./ldap-test-tool search multi -h
Search Multi Users

Usage:
  ldap-test-tool search multi [filename] [flags]

Flags:
  -f, --file   output search to users.csv, failed search to failed.csv
  -h, --help   help for multi

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

示例

# cat searchusers.txt 
qfeng
qfengtest
nofounduser

searchuser.txt 中有三个用户,其中 nofounduser 是不存在的用户

# ldap-test-tool.exe search multi .\searchusers.txt
LDAP Multi Search Start
==================================

Successed users:

DN: uid=qfeng,ou=people,dc=example,dc=org
Attributes:
 -- uid  : qfeng
 -- cn   : 冯骐
 -- mail : qfeng@example.org


DN: uid=qfengtest,ou=people,dc=example,dc=org
Attributes:
 -- uid  : qfengtest
 -- cn   : 冯骐测试
 -- mail : qfeng@example.org

nofounduser : Cannot find such user

Successed count 2
Failed count 1

==================================
LDAP Multi Search Finished, Time Usage 134.744ms

当使用 -f 选项时,查询的结果将输出到 csv 中。csv 将以配置文件中 attributes 的属性作为 title。因此当使用 -f 选项时,attributes 不得为空。

# ./ldap-test-tool search multi searchusers.txt -f
LDAP Multi Search Start 
==================================

OutPut to csv successed

==================================
LDAP Multi Search Finished, Time Usage 88.756956ms 

# ls | grep csv
failed.csv
users.csv

HTTP API

HTTP API 部分使用 beego 框架
使用如下命令开启 HTTP API

# ldap-test-tool.exe http
2018/03/12 14:30:25 [I] http server Running on http://0.0.0.0:8888
健康状态

检测 ldap 健康状态

# curl http://127.0.0.1:8888/api/v1/ldap/health   
{
  "msg": "ok",
  "success": true
}
查询用户

查询单个用户信息

# curl  http://127.0.0.1:8888/api/v1/ldap/search/user/qfeng
{
  "user": {
    "dn": "uid=qfeng,ou=people,dc=example,dc=org",
    "attributes": {
      "cn": [
        "冯骐"
      ],
      "mail": [
        "qfeng@example.org"
      ],
      "uid": [
        "qfeng"
      ]
    }
  },
  "success": true
}
Filter 查询

根据 LDAP Filter 查询

# curl  http://127.0.0.1:8888/api/v1/ldap/search/filter/\(cn=*测试\)
{
  "results": [
    {
      "dn": "uid=test1,ou=people,dc=example,dc=org",
      "attributes": {
        "cn": [
          "一号测试"
        ],
        "mail": [
          "test1@example.org"
        ],
        "uid": [
          "test1"
        ]
      }
    },
    {
      "dn": "uid=test2,ou=people,dc=example,dc=org",
      "attributes": {
        "cn": [
          "二号测试"
        ],
        "mail": [
          "test2@example.org"
        ],
        "uid": [
          "test2"
        ]
      }
    },
    {
      "dn": "uid=test3,ou=people,dc=example,dc=org",
      "attributes": {
        "cn": [
          "三号测试"
        ],
        "mail": [
          "test3@example.org"
        ],
        "uid": [
          "test3"
        ]
      }
    },
  ],
  "success": true
}
多用户查询

同时查询多个用户,以 application/json 方式发送请求数据,请求数据示例

["qfeng","qfengtest","nofounduser"]

curl 示例

# curl -X POST  -H 'Content-Type:application/json' -d '["qfeng","qfengtest","nofounduser"]' http://127.0.0.1:8888/api/v1/ldap/search/multi
{
  "success": true,
  "result": {
    "successed": 2,
    "failed": 1,
    "users": [
      {
        "dn": "uid=qfeng,ou=people,dc=example,dc=org",
        "attributes": {
          "cn": [
            "冯骐"
          ],
          "mail": [
            "qfeng@example.org"
          ],
          "uid": [
            "qfeng"
          ]
        }
      },
      {
        "dn": "uid=qfengtest,ou=people,dc=example,dc=org",
        "attributes": {
          "cn": [
            "冯骐测试"
          ],
          "mail": [
            "qfeng@example.org"
          ],
          "uid": [
            "qfengtest"
          ]
        }
      }
    ],
    "failed_messages": [
      {
        "username": "nofounduser",
        "message": "Cannot find such user"
      }
    ]
  }
}

认证

单用户认证

单个用户认证测试,以 application/json 方式发送请求数据,请求数据示例

{
    "username": "qfeng",
    "password": "123456"
}

curl 示例

# curl -X POST  -H 'Content-Type:application/json' -d '{"username":"qfeng","password":"123456"}' http://127.0.0.1:8888/api/v1/ldap/auth/single
{
  "msg": "user 20150073 Auth Successed",
  "success": true
}
多用户认证

同时发起多个用户认证测试,以 application/json 方式发送请求数据,请求数据示例

[{
    "username": "qfeng",
    "password": "123456"
}, {
    "username": "qfengtest",
    "password": "1111111"
}]

curl 示例

# curl -X POST  -H 'Content-Type:application/json' -d '[{"username":"qfeng","password":"123456"},{"username":"qfengtest","password":"1111111"}]' http://127.0.0.1:8888/api/v1/ldap/auth/multi
{
  "success": true,
  "result": {
    "successed": 1,
    "failed": 1,
    "failed_messages": [
      {
        "username": "qfengtest",
        "message": "LDAP Result Code 49 \"Invalid Credentials\": "
      }
    ]
  }
}

参考文档

LDAP WiKi
SSL vs TLS vs STARTTLS
IBM Security Identity Manager V6.0.0.10 - enRoleLDAPConnection.properties
RFC4511
cobra
beego

以上

转载授权

CC BY-SA

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

推荐阅读更多精彩内容