k8s之service account

service account是k8s为pod内部的进程访问apiserver创建的一种用户。其实在pod外部也可以通过sa的token和证书访问apiserver,不过在pod外部一般都是采用client 证书的方式。

创建一个namespace,就会自动生成名字为 default 的 service account。

root@master:~# kubectl create ns test
namespace/test created
root@master:~# kubectl get sa -n test
NAME      SECRETS   AGE
default   1         6s

当然我们也可以再创建额外的sa。

root@master:~# kubectl create sa sa1 -n test
serviceaccount/sa1 created
root@master:~# kubectl get sa -n test
NAME      SECRETS   AGE
default   1         94s
sa1       1         2s

有了sa后,我们就可以使用sa的token和apiserver交互了,由于所有通信都通过TLS进行,所以也得需要证书(ca.crt,这里的证书指的是server端的ca证书)或者允许不安全的连接(--insecure)。
token和证书如何获取的?每个sa都会自动关联一个secret,token和证书就存在secret中。在pod内部他们被放在如下文件中(所有pod内部的ca.crt证书都一样,都是/etc/kubernetes/pki/ca.crt)

/var/run/secrets/kubernetes.io/serviceaccount/token
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

在外部可以通过secret获取。下面分别实验这两种方式下如何访问apiserver。

root@master:~# kubectl describe sa sa1 -n test
Name:                sa1
Namespace:           test
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   sa1-token-p5wxt
Tokens:              sa1-token-p5wxt
Events:              <none>

外部访问apiserver

下面验证在外部如何通过sa的token和证书访问apiserver。

首先获取sa的token,cert和apiserver endpoint。

SERVICE_ACCOUNT=sa1

# Get the ServiceAccount's token Secret's name
SECRET=$(kubectl get serviceaccount -n test ${SERVICE_ACCOUNT} -o json | jq -Mr '.secrets[].name | select(contains("token"))')
 
# Extract the Bearer token from the Secret and decode
TOKEN=$(kubectl get secret -n test ${SECRET} -o json | jq -Mr '.data.token' | base64 -d)
 
# Extract, decode and write the ca.crt to a temporary location
kubectl get secret -n test ${SECRET} -o json | jq -Mr '.data["ca.crt"]' | base64 -d > /tmp/ca.crt
 
# Get the API Server location
APISERVER=$(kubectl config view --minify | grep server | cut -f 2- -d ":" | tr -d " ")

使用curl命令,指定token和insecure(表示不对server端证书进行认证),开始和apiserver交互。

root@master:~# curl --header "Authorization: Bearer $TOKEN" --insecure -s $APISERVER/api
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "192.168.122.20:6443"
    }
  ]
}root@master:~#

也可以通过 --cacert /tmp/ca.crt 指定证书。

curl的参数--cacert 的作用
(HTTPS) Tells curl to use the specified certificate file to verify the peer. The file may contain multiple CA certificates. The certificate(s) must be in PEM format. If this option is used several times, the last one will be used.

root@master:~# curl --header "Authorization: Bearer $TOKEN" --cacert /tmp/ca.crt -s $APISERVER/api/
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "192.168.122.20:6443"
    }
  ]
}root@master:~#

pod内部访问apiserver

首先创建一个包含curl命令的pod,虽然没有指定sa,但是会自动将test namespace下的default的sa分配给这个pod。

root@master:~# cat <<EOF | kubectl create -f -
> apiVersion: v1
> kind: Pod
> metadata:
>   name: test
>   namespace: test
>
> spec:
>   containers:
>   - name: samplepod
>     command: ["/bin/sh", "-c", "sleep 99999"]
>     image: byrnedo/alpine-curl
> EOF
pod/test created

进入pod内部,获取token,crt。注意的是在pod内部是通过下面的两个环境变量获取apiserver的endpoint的,这里的endpoint是service ip和port,即10.96.0.10:443,而在pod外部使用的endpoint是192.168.122.20:6443.
KUBERNETES_SERVICE_HOST
KUBERNETES_PORT_443_TCP_PORT

root@master:~# kubectl exec -it -n test test sh
获取token和证书
/ # TOKEN=`cat /var/run/secrets/kubernetes.io/serviceaccount/token`
/ # APISERVER="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_PORT_443_TCP_PORT"
不使用证书访问
/ # curl --header "Authorization: Bearer $TOKEN" --insecure -s $APISERVER/api
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "192.168.122.20:6443"
    }
  ]
}/ #
使用证书访问
/ # CAPATH="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
/ # curl --header "Authorization: Bearer $TOKEN" --cacert $CAPATH -s $APISERVER/api
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "192.168.122.20:6443"
    }
  ]
}/ #

sa的默认权利

当curl请求通过apiserver的认证后,会被分配一个user - system:serviceaccount:test:sa1,和一个group - system:serviceaccounts:test:sa1,同时也会被分配另一个group system:authenticated代表这是一个通过认证的请求。

前面的user和group目前是没有关联任何role或者clusterrole的,这意味着他们是没有任何权利去查看或者修改k8s内部资源的。而system:authenticated是系统自动创建的group,并且已经被默认关联到了下面的三个clusterrole,他们是有查看资源的权利,但是很受限

system:public-info-viewer
system:discovery
system:basic-user

通过下面的clusterrolebinding可看到,上面的三个clusterrole确实绑定到group system:authenticated了。

root@master:~# kubectl describe clusterrolebinding system:public-info-viewer
Name:         system:public-info-viewer
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  system:public-info-viewer
Subjects:
  Kind   Name                    Namespace
  ----   ----                    ---------
  Group  system:authenticated
  Group  system:unauthenticated

root@master:~# kubectl describe clusterrolebinding system:discovery
Name:         system:discovery
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  system:discovery
Subjects:
  Kind   Name                  Namespace
  ----   ----                  ---------
  Group  system:authenticated

root@master:~# kubectl describe clusterrolebinding system:basic-user
Name:         system:basic-user
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  system:basic-user
Subjects:
  Kind   Name                  Namespace
  ----   ----                  ---------
  Group  system:authenticated

通过下面的命令查看这三个clusterrole都有什么权利,可以看到权利是比较低的,只能查看Non-Resource URLs,不能查看pod,namespace,deployment等资源信息。

root@master:~# kubectl describe clusterrole system:public-info-viewer
Name:         system:public-info-viewer
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
             [/healthz]         []              [get]
             [/livez]           []              [get]
             [/readyz]          []              [get]
             [/version/]        []              [get]
             [/version]         []              [get]

root@master:~# kubectl describe clusterrole system:discovery
Name:         system:discovery
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
             [/api/*]    []              [get]
             [/api]             []              [get]
             [/apis/*]          []              [get]
             [/apis]            []              [get]
             [/healthz]         []              [get]
             [/livez]           []              [get]
             [/openapi/*]       []              [get]
             [/openapi]         []              [get]
             [/readyz]          []              [get]
             [/version/]        []              [get]
             [/version]         []              [get]

root@master:~# kubectl describe clusterrole system:basic-user
Name:         system:basic-user
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources                                      Non-Resource URLs  Resource Names  Verbs
  ---------                                      -----------------  --------------  -----
  selfsubjectaccessreviews.authorization.k8s.io  []                 []              [create]
  selfsubjectrulesreviews.authorization.k8s.io   []                 []              [create]

尝试获取pod信息,但是被Forbidden,因为没有被授权。

root@master:~# curl --header "Authorization: Bearer $TOKEN" --insecure -s $APISERVER/api/v1/namespaces/test
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {

  },
  "status": "Failure",
  "message": "namespaces \"test\" is forbidden: User \"system:serviceaccount:test:sa1\" cannot get resource \"namespaces\" in API group \"\" in the namespace \"test\"",
  "reason": "Forbidden",
  "details": {
    "name": "test",
    "kind": "namespaces"
  },
  "code": 403

提高sa权利

如何提高sa的权利呢?
a. 修改默认的这三个clusterrole,但是这是公共的,不建议修改。
b. 将sa绑定的其他权利比较高的clusterrole,比如cluster-admin。
c. 新创建一个role或者clusterrole,指定好需要的权利,将sa绑定上即可。这是推荐的做法。

下面采用第三种方法进行验证。
在test namespace创建一个role read-pod,这个role的权利只可以获取namespace test下的pod。

root@master:~# cat <<EOF | kubectl create -f -
> apiVersion: rbac.authorization.k8s.io/v1
> kind: Role
> metadata:
>   namespace: test
>   name: read-pod
> rules:
> - apiGroups: [""]
>   resources: ["pods"]
>   verbs: ["get", "list"]
> EOF
role.rbac.authorization.k8s.io/read-pod created
root@master:~# kubectl create rolebinding test  -n test --role read-pod --serviceaccount test:sa1
rolebinding.rbac.authorization.k8s.io/test created

验证一下,可以获取test namespace下的pod

root@master:~# curl --header "Authorization: Bearer $TOKEN" --cacert /tmp/ca.crt -s $APISERVER/api/v1/namespaces/test/pods/test
{
  "kind": "Pod",
  "apiVersion": "v1",
  "metadata": {
    "name": "test",
    "namespace": "test",
    "selfLink": "/api/v1/namespaces/test/pods/test",
    "uid": "12ef72cf-be59-4329-8e67-2f3c805a553f",
    "resourceVersion": "13801401",
    "creationTimestamp": "2020-08-22T22:50:22Z",
    "annotations": {
      "cni.projectcalico.org/podIP": "10.24.166.144/32",
      "cni.projectcalico.org/podIPs": "10.24.166.144/32",
      "k8s.v1.cni.cncf.io/network-status": "[{\n    \"name\": \"k8s-pod-network\",\n    \"ips\": [\n        \"10.24.166.144\"\n    ],\n    \"default\": true,\n    \"dns\": {}\n}]",
      "k8s.v1.cni.cncf.io/networks-status": "[{\n    \"name\": \"k8s-pod-network\",\n    \"ips\": [\n        \"10.24.166.144\"\n    ],\n    \"default\": true,\n    \"dns\": {}\n}]"
    }
  },
  ...

但是pod的子资源是不能获取的,比如获取pods/logs,因为role里只指定了pod资源。如果想获取子资源,还得单独指定。

root@master:~# curl --header "Authorization: Bearer $TOKEN" --cacert /tmp/ca.crt -s $APISERVER/api/v1/namespaces/test/pods/test/logs
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {

  },
  "status": "Failure",
  "message": "pods \"test\" is forbidden: User \"system:serviceaccount:test:sa1\" cannot get resource \"pods/logs\" in API group \"\" in the namespace \"test\"",
  "reason": "Forbidden",
  "details": {
    "name": "test",
    "kind": "pods"
  },
  "code": 403

Non-resource requests 和 resource requests

下面一段话是官网对这两个概念的解释,但是还是不太明白什么意思。
Non-resource requests Requests to endpoints other than /api/v1/... or /apis/<group>/<version>/... are considered "non-resource requests", and use the lower-cased HTTP method of the request as the verb. For example, a GET request to endpoints like /api or /healthz would use get as the verb.

Resource requests To determine the request verb for a resource API endpoint, review the HTTP verb used and whether or not the request acts on an individual resource or a collection of resources:

而且查看clusterrole时,也把这两种请求区分开来,如下

root@master:~# kubectl describe clusterrole system:discovery
Name:         system:discovery
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources  Non-Resource URLs     Resource Names  Verbs
  ---------  -----------------     --------------  -----
             [/api/*]              []              [get]
             [/api]                []              [get]
             [/apis/*]             []              [get]
             [/apis]               []              [get]
             [/healthz]            []              [get]
             [/livez]              []              [get]
             [/openapi/*]          []              [get]
             [/openapi]            []              [get]
             [/readyz]             []              [get]
             [/version/]           []              [get]
             [/version]            []              [get]

所以看了下源码,如果请求的url path满足下面的三个条件之一的话就是Non-Resource request,否则就是 resource request。
a. 请求url path字段小于3,比如
/livez(一个字段),/api(一个字段), /api/v1(两个字段)等
b. 如果请求url path大于等于3了,但是url path不是以 api 或者 apis开始。比如 /livez/poststarthook/crd-informer-synced
c. url path以/apis开始的,但是后面的字段小于3,比如
/apis/{api-group}或者 /apis/{api-group}/{version}

代码路径
./staging/src/k8s.io/apiserver/pkg/endpoints/request/requestinfo.go

如下结构体用于保存解析http请求的内容
// RequestInfo holds information parsed from the http.Request
type RequestInfo struct {
    // IsResourceRequest indicates whether or not the request is for an API resource or subresource
    IsResourceRequest bool
    // Path is the URL path of the request
    Path string
    // Verb is the kube verb associated with the request for API requests, not the http verb.  This includes things like list and watch.
    // for non-resource requests, this is the lowercase http verb
    Verb string

    APIPrefix  string
    APIGroup   string
    APIVersion string
    Namespace  string
    // Resource is the name of the resource being requested.  This is not the kind.  For example: pods
    Resource string
    // Subresource is the name of the subresource being requested.  This is a different resource, scoped to the parent resource, but it may have a different kind.
    // For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
    // (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
    Subresource string
    // Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
    Name string
    // Parts are the path parts for the request, always starting with /{resource}/{name}
    Parts []string
}

func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, error) {
    // start with a non-resource request until proven otherwise
    requestInfo := RequestInfo{
        IsResourceRequest: false,
        Path:              req.URL.Path,
        Verb:              strings.ToLower(req.Method),
    }
    //如果请求的字段小于3,则认为是 no-resource 请求,比如 /healthz,/readyz
    currentParts := splitPath(req.URL.Path)
    if len(currentParts) < 3 {
        // return a non-resource request
        return &requestInfo, nil
    }
    //不是以 api 或者 apis开始的都认为是 no-resource 请求,
    if !r.APIPrefixes.Has(currentParts[0]) {
        // return a non-resource request
        return &requestInfo, nil
    }
    requestInfo.APIPrefix = currentParts[0]
    currentParts = currentParts[1:]
    //走到这里说明url path开始是api或者是apis。
    //下面的判断是如果不是api开始的,就是说以apis开始的请求。
    if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) {
        //apis开始的请求,如果后面的字段小于3,也表示 non-resource 请求,比如 /apis/apiregistration.k8s.io/v1
        // one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?"
        if len(currentParts) < 3 {
            // return a non-resource request
            return &requestInfo, nil
        }
        requestInfo.APIGroup = currentParts[0]
        currentParts = currentParts[1:]
    }

    requestInfo.IsResourceRequest = true
    requestInfo.APIVersion = currentParts[0]
    currentParts = currentParts[1:]

    // handle input of form /{specialVerb}/*
    if specialVerbs.Has(currentParts[0]) {
        if len(currentParts) < 2 {
            return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
        }

        requestInfo.Verb = currentParts[0]
        currentParts = currentParts[1:]

    } else {
        switch req.Method {
        case "POST":
            requestInfo.Verb = "create"
        case "GET", "HEAD":
            requestInfo.Verb = "get"
        case "PUT":
            requestInfo.Verb = "update"
        case "PATCH":
            requestInfo.Verb = "patch"
        case "DELETE":
            requestInfo.Verb = "delete"
        default:
            requestInfo.Verb = ""
        }
    }

    // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind
    if currentParts[0] == "namespaces" {
        if len(currentParts) > 1 {
            requestInfo.Namespace = currentParts[1]

            // if there is another step after the namespace name and it is not a known namespace subresource
            // move currentParts to include it as a resource in its own right
            if len(currentParts) > 2 && !namespaceSubresources.Has(currentParts[2]) {
                currentParts = currentParts[2:]
            }
        }
    } else {
        requestInfo.Namespace = metav1.NamespaceNone
    }
    // parsing successful, so we now know the proper value for .Parts
    requestInfo.Parts = currentParts

    // parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
    switch {
    case len(requestInfo.Parts) >= 3 && !specialVerbsNoSubresources.Has(requestInfo.Verb):
        requestInfo.Subresource = requestInfo.Parts[2]
        fallthrough
    case len(requestInfo.Parts) >= 2:
        requestInfo.Name = requestInfo.Parts[1]
        fallthrough
    case len(requestInfo.Parts) >= 1:
        requestInfo.Resource = requestInfo.Parts[0]
    }

    // if there's no name on the request and we thought it was a get before, then the actual verb is a list or a watch
    if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" {
        opts := metainternalversion.ListOptions{}
        if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, &opts); err != nil {
            // An error in parsing request will result in default to "list" and not setting "name" field.
            klog.Errorf("Couldn't parse request %#v: %v", req.URL.Query(), err)
            // Reset opts to not rely on partial results from parsing.
            // However, if watch is set, let's report it.
            opts = metainternalversion.ListOptions{}
            if values := req.URL.Query()["watch"]; len(values) > 0 {
                switch strings.ToLower(values[0]) {
                case "false", "0":
                default:
                    opts.Watch = true
                }
            }
        }

        if opts.Watch {
            requestInfo.Verb = "watch"
        } else {
            requestInfo.Verb = "list"
        }

        if opts.FieldSelector != nil {
            if name, ok := opts.FieldSelector.RequiresExactMatch("metadata.name"); ok {
                if len(path.IsValidPathSegmentName(name)) == 0 {
                    requestInfo.Name = name
                }
            }
        }
    }
    // if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
    if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
        requestInfo.Verb = "deletecollection"
    }

    return &requestInfo, nil
}

从上述源码也能看出 http verb如何转换成 request verb

POST -> create
GET/HEAD with resourceName -> get
GET/HEAD without resourceName -> list(如果没有指定资源名字,则列出所有的资源,比如指定了获取pod1 /api/v1/namespaces/test/pods/pod1,则只获取pod1的信息,如果不指定pod名字,就会返回test namespace下的所有pod)
PUT-> update
PATCH->patch
DELETE with resourceName  ->delete
DELETE without resourceName  ->delete(同get,如果没有指定删除具体的资源,则删除所有的资源)

Non-resource requests只能在clusterrole中配置,而resource requests可以在role或者clusterrole中配置。
Non-resource requests 和resource requests的配置格式也不一样,如下

//resource requests
rules:
- apiGroups: [""]
  #
  # at the HTTP level, the name of the resource for accessing Node
  # objects is "nodes"
  resources: ["nodes"]
  verbs: ["get", "list", "watch"]
//Non-resource requests
rules:
- nonResourceURLs: ["/healthz", "/healthz/*"] # '*' in a nonResourceURL is a suffix glob match
  verbs: ["get", "post"]

参考

service account相关
https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/
授权相关
https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/
https://kubernetes.io/docs/reference/access-authn-authz/rbac/
https://kubernetes.io/docs/reference/access-authn-authz/authorization/
https://kubernetes.io/docs/reference/access-authn-authz/authentication/

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