Spring Cloud Gateway(一、路由转发及自定义过滤器)

1. 概述

Spring Cloud Gateway 是由 WebFlux + Netty + Reactor 实现的响应式的 API 网关。

Spring Cloud Gateway 旨在为微服务架构提供一种简单且有效的 API 路由的管理方式,并基于 Filter 的方式提供网关的基本功能,例如说安全认证、监控、限流等等。

目前Spring Cloud集成的Spring Cloud Zuul还是Zuul1.x,这一版的Zuul是基于Servlet构建的,采用的方案是阻塞式的多线程方案,即一个线程处理一次连接请求,这种方式在内部延迟严重、设备故障较多情况下会引起存活的连接增多和线程增加的情况发生。Spring Cloud自己开源的Spring Cloud Gateway则是基于Spring Webflux来构建的,Spring Webflux有一个全新的非堵塞的函数式 Reactive Web 框架,可以用来构建异步的、非堵塞的、事件驱动的服务,在伸缩性方面表现非常好。使用非阻塞API, Websockets得到支持,并且由于它与Spring紧密集成,将会得到更好的开发体验。

Spring Cloud Gateway 定位于取代 Netflix Zuul,成为 Spring Cloud 生态系统的新一代网关。目前看下来非常成功,老的项目的网关逐步从 Zuul 迁移到 Spring Cloud Gateway,新项目的网关直接采用 Spring Cloud Gateway。相比 Zuul 来说,Spring Cloud Gateway 提供更优秀的性能,更强大的有功能

服务网关 = 路由转发 + 过滤器

  1. 路由转发:接收一切外界请求,转发到后端的微服务上去;
  2. 过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成(其实路由转发也是通过过滤器实现的)。

Spring Cloud Gateway 的特征如下:

  1. 基于 Java 8 编码
  2. 基于 Spring Framework 5 + Project Reactor + Spring Boot 2.0 构建
  3. 支持动态路由,能够匹配任何请求属性上的路由
  4. 支持内置到 Spring Handler 映射中的路由匹配
  5. 支持基于 HTTP 请求的路由匹配(Path、Method、Header、Host 等等)
  6. 集成了 Hystrix 断路器
  7. 过滤器作用于匹配的路由
  8. 过滤器可以修改 HTTP 请求和 HTTP 响应(增加/修改 Header、增加/修改请求参数、改写请求 Path 等等)
  9. 支持 Spring Cloud DiscoveryClient 配置路由,与服务发现与注册配合使用
    支持限流
  10. 支持WebSocket 的网关。

2. 为什么使用网关?

API网关接管所有的入口流量,类似Nginx的作用,将所有用户的请求转发给后端的服务器,但网关做的不仅仅只是简单的转发,也会针对流量做一些扩展,比如鉴权、限流、权限、熔断、协议转换、错误码统一、缓存、日志、监控、告警等,这样将通用的逻辑抽出来,由网关统一去做,业务方也能够更专注于业务逻辑,提升迭代的效率。 通过引入API网关,客户端只需要与API网关交互,而不用与各个业务方的接口分别通讯,但多引入一个组件就多引入了一个潜在的故障点,因此要实现一个高性能、稳定的网关,也会涉及到很多点。

3. 快速入门——基于配置中心 Nacos 实现动态路由

主要介绍以Nacos为配置中心,实现Spring Cloud GateWay 实现动态路由的功能。这样做是因为:Spring Cloud Gateway启动时候,就将路由配置和规则加载到内存里,无法做到不重启网关就可以动态的对应路由的配置和规则进行增加,修改和删除。通过nacos的配置下发的功能可以实现在不重启网关的情况下,实现动态路由。
所以我们可以引入配置中心 Nacos 来实现动态路由的功能,将spring.cloud.gateway配置项统一存储在 Nacos 中。在服务网关Spring Cloud Gateway中开启监听,监听Nacos配置文件的修改。
Nacos配置文件一旦发生改变,则Spring Cloud Gateway重新刷新自己的路由信息。

4. 用户服务

创建 [sc-user-service]项目,作为 user-service 用户服务。代码比较简单。最终项目如下图所示:

UserController

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sc-user-service</artifactId>

    <properties>
        <spring.boot.version>2.2.4.RELEASE</spring.boot.version>
        <spring.cloud.version>Hoxton.SR1</spring.cloud.version>
        <spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
    </properties>

    <!--
        引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
        在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
     -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 引入 SpringMVC 相关依赖,并实现对其的自动配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

</project>

application.yaml

spring:
  application:
    name: user-service # Spring 应用名
  cloud:
    nacos:
      # Nacos 作为注册中心的配置项
      discovery:
        server-addr: 127.0.0.1:8848 # Nacos 服务器地址 "'lb://'+serviceId"

server:
  port: ${random.int[10000,19999]}  # 服务器端口。默认为 8080
  #  port: 18080  # 服务器端口。默认为 8080

注意:端口用的是随机配置,不是通常的固定端口号方式。本项目是服务提供者,需要启动多个进行负载均衡,所以启动该项目后,勾选Allow parallel run,即可同时启动多个服务。

Allow parallel run

启动2次用户服务,随机使用了不冲突的2个端口:
点击停止按钮会出现2个UserServiceApplication

在 Nacos 注册中心可以看到该服务的两个实例,如下图所示:
同一个服务的2个实例被注册到Nacos

5. 网关服务

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.guo.springcloud</groupId>
    <artifactId>sc-gateway-demo03-config-nacos</artifactId>
    <version>1.0.0.RELEASE</version>

    <properties>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
        <spring.boot.version>2.2.4.RELEASE</spring.boot.version>
        <spring.cloud.version>Hoxton.SR1</spring.cloud.version>
        <spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
    </properties>

    <!--
        引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
        在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
     -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 引入 Spring Cloud Gateway 相关依赖,使用它作为网关,并实现对其的自动配置 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- 引入 Spring Cloud Alibaba Nacos Config 相关依赖,将 Nacos 作为配置中心,并实现对其的自动配置 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>

</project>

其中引入配置中心 Nacos 相关的依赖如下:

<!-- 引入 Spring Cloud Alibaba Nacos Config 相关依赖,将 Nacos 作为配置中心,并实现对其的自动配置 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

配置文件

① 创建 bootstrap.yaml 配置文件,添加配置中心 Nacos 相关的配置。配置如下:

spring:
  application:
    name: gateway-application

  cloud:
    nacos:
      # Nacos Config 配置项,对应 NacosConfigProperties 配置属性类
      config:
        server-addr: 127.0.0.1:8848 # Nacos 服务器地址
        namespace: # 使用的 Nacos 的命名空间,默认为 null
        group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
        name: # 使用的 Nacos 配置集的 dataId,默认为 spring.application.name
        file-extension: yaml # 使用的 Nacos 配置集的 dataId 的文件拓展名,同时也是 Nacos 配置集的配置格式,默认为 properties

spring.cloud.nacos.config 配置项,为配置中心 Nacos 相关配置项。这里就不详细解释,毕竟 Nacos 不是主角。感兴趣的,可以阅读Spring Cloud Alibaba(三、Nacos配置中心)文章。

② 修改 application.yaml 配置文件,删除 Gateway 相关的硬编码配置。完整配置如下:

server:
  port: 8888

spring:

  cloud:
    # Spring Cloud Gateway 配置项,全部配置在 Nacos 中
    # gateway:

    # Nacos 作为注册中心的配置项
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # Nacos 服务器地址

spring.cloud.gateway 配置项,我们都删除了,统一在配置中心 Nacos 中进行配置。

为了演示 Gateway 启动时,从 Nacos 加载 spring.cloud.gateway 配置项,作为初始的路由信息,我们在 Nacos 配置如下:

网关的Nacos配置项

默认将请求转发到163网站。

6. 测试

① 执行 UserServiceApplication启动 两次,启动两个 user-service 服务。
② 执行 GatewayApplication 启动网关。
使用浏览器,访问 http://127.0.0.1:8888/ 地址,返回 163首页。
③ 修改在 Nacos 的 spring.cloud.gateway 配置项,转发请求到用户服务。
配置对应文本内容如下:

spring:
  cloud:
    # Spring Cloud Gateway 配置项,对应 GatewayProperties 类
    gateway:
      # 路由配置项,对应 RouteDefinition 数组
      routes:
        - id: ReactiveCompositeDiscoveryClient_user-service # 路由的编号
          uri: lb://user-service # 路由到的目标地址
          predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
            - Path=/user/**
          filters:
            - StripPrefix=1

配置说明:
spring.cloud.nacos.discovery 配置项,使用 Nacos 作为 Spring Cloud 注册中心的配置项。这里就不详细解释,毕竟 Nacos 不是主角。

spring.cloud.gateway.discovery 配置项,Gateway 与 Spring Cloud 注册中心的集成,对应 DiscoveryLocatorProperties类。

  • enable:是否开启,默认为 false 关闭。这里我们设置为 true,开启与 Spring Cloud 注册中心的集成的功能。
  • url-expression:路由的目标地址的 Spring EL 表达式,默认为 "'lb://' + serviceId"。这里,我们设置的就是默认值。

可能 url-expression 配置项有点费解,我们来重点解释下。

  • lb:// 前缀,表示将请求负载均衡转发到对应的服务的实例。
  • "'lb://' + serviceId" Spring EL 表达式,将从注册中心获得到的服务列表,每一个服务的名字对应 serviceId,最终使用 Spring EL 表达式进行格式化。
  • ANT通配符有三种:


    通配符解释

我们来举个例子,假设我们从注册中心找到了 user-serviceorder-service 两个服务,最终效果和如下配置等价:

spring:
 cloud:
 gateway:
 routes:
 - id: ReactiveCompositeDiscoveryClient_user-service
 uri: lb://user-service
 predicates:
 - Path=/user-service/**
 filters:
 - RewritePath=/user-service/(?<remaining>.*), /${remaining} # 将 /user-service 前缀剔除
 - id: ReactiveCompositeDiscoveryClient_order-service
 uri: lb://order-service
 predicates:
 - Path=/order-service/**
 filters:
 - RewritePath=/order-service/(?<remaining>.*), /${remaining} # 将 /order-service 前缀剔除

此时 IDEA 控制台看到 GatewayPropertiesRefresher 监听到 spring.cloud.gateway 配置项刷新,并打印日志如下图:

探测到路由规则更新

④ 访问 http://127.0.0.1:8888/user/user/get?id=8 地址,返回 JSON 结果如下:
{"id":8,"name":"8:没有昵称","gender":1}

至此,请求经过网关后,转发到 user-service 服务成功。

7.自定义过滤器

可以实现全局日志记录,统一网关鉴权等功能。

7.1 新增lombok依赖:

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

7.2 新建自定义过滤器类

7.2.1 例子1,参数方式判断

package com.erbadagang.springcloud.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @description 自定义过滤器
 * @ClassName: MyFilter
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/11 16:33
 * @Copyright:
 */
@Component
@Slf4j
public class MyFilter implements GlobalFilter, Ordered {
    /**
     * 表示这个过滤器的优先级,数字越小优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("=================进入自定义全局过滤器=============");
        // 获取id参数
        String id = exchange.getRequest().getQueryParams().getFirst("id");
        if (!"8".equals(id)) {
            log.error("================= id不是8==================");
            // 设置返回码
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }
}

也就是说,如果没有id这个参数或者id参数值不为8,都会被拦截。实际生产中可以用这个来验证请求是否携带token

7.2.2 例子2:修改gateway请求路径

private final static String SEGMENT = "-";
 
  /**
   * Process the Web request and (optionally) delegate to the next {@code WebFilter} through the
   * given {@link GatewayFilterChain}.
   *
   * @param exchange the current server exchange
   * @param chain provides a way to delegate to the next filter
   * @return {@code Mono<Void>} to indicate when request processing is complete
   */
  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponse response = exchange.getResponse();
    // 获取请求路径前缀(api-dev/api-prod)
    PathPattern.PathMatchInfo pathMatchInfo = exchange
        .getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE);
    if (pathMatchInfo != null) {
      Map<String, String> uriVariables = pathMatchInfo.getUriVariables();
      String api = uriVariables.get("api");
      // 判断请求路径是否符合当前环境
      if (!api.split(SEGMENT)[1].equals(active)) {
        byte[] responseBytes = JSONUtil
            .toJsonStr(Response
                .failed(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()))
            .getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(responseBytes);
        response.setStatusCode(HttpStatus.FORBIDDEN);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
      }
    }
 
    // 将StripPrefixGatewayFilter加入全局过滤器中
    addOriginalRequestUrl(exchange, request.getURI());
    String rawPath = request.getURI().getRawPath();
    String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(rawPath, "/"))
        .skip(2L).collect(Collectors.joining("/"));
    ServerHttpRequest newRequest = request.mutate()
        .path(newPath)
        .build();
    exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());
 
    return chain.filter(exchange.mutate()
        .request(newRequest).build());
  }

7.2.3 例子3:Path Route Predicate Factory

Path Route Predicate Factory使用的是path列表作为参数,使用Spring的PathMatcher匹配path,可以设置可选变量。 application.yml:

spring:  
    cloud:    
        gateway:      
            routes:      
            - id: host_route        
              uri: http://www.google.com       
              predicates:        
              - Path=/foo/{segment},/bar/{segment}

上面路由可以匹配诸如:/foo/1 或 /foo/bar 或 /bar/baz等 其中的segment变量可以通过下面方式获取:

PathMatchInfo variables = exchange.getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE);
Map<String, String> uriVariables = variables.getUriVariables();
String segment = uriVariables.get("segment");

在后续的GatewayFilter Factories就可以做对应的操作了。

7.2.4 例子4:重定向(redirect)到指定页面

对于浏览器,通常是发现没有权限后跳转到登录页面。响应状态码需要为HttpStatus.SEE_OTHER(303)。

重定向(redirect)会丢失之前请求的参数,对于需要转发到目标URL的参数,需手工添加。

import java.net.URI;
import java.nio.charset.StandardCharsets;
 
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
 
@Component
public class AuthFilter implements GlobalFilter, Ordered {
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("authToken");
        //重定向(redirect)到登录页面
        if (StringUtils.isBlank(token)) {
            String url = "http://想跳转的网址";
            ServerHttpResponse response = exchange.getResponse();
            //303状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
            response.setStatusCode(HttpStatus.SEE_OTHER);
            response.getHeaders().set(HttpHeaders.LOCATION, url);
            return response.setComplete();
        }
        return chain.filter(exchange);
    }
 
    @Override
    public int getOrder() {
        return -100;
    }
}

7.2.5 返回401状态码和提示信息

只要将自定义的GlobalFilter声明成Spring Bean就会自动生效,Ordered接口用来指定拦截器生效顺序(数字越小优先级越高)。

这里假设用来验证权限的key是authToken。

import java.net.URI;
import java.nio.charset.StandardCharsets;
 
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
 
import com.alibaba.fastjson.JSONObject;
 
@Component
public class AuthFilter implements GlobalFilter, Ordered {
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("authToken");
        //返回401状态码和提示信息
        if (StringUtils.isBlank(token)) {
            ServerHttpResponse response = exchange.getResponse();
            JSONObject message = new JSONObject();
            message.put("status", -1);
            message.put("data", "鉴权失败");
            byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = response.bufferFactory().wrap(bits);
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            //指定编码,否则在浏览器中会中文乱码
            response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
            return response.writeWith(Mono.just(buffer));
        }
        return chain.filter(exchange);
    }
 
    @Override
    public int getOrder() {
        return -100;
    }
}

7.3 测试

  1. 访问 http://127.0.0.1:8888/user/user/get?id=8 地址,正确返回 JSON 结果如下:
    {"id":8,"name":"8:没有昵称","gender":1}
  2. 访问 http://127.0.0.1:8888/user/user/get?id=1 地址,无任何返回信息。并打印出filter的日志:
    id不是8的异常拦截

8.本文源码

本文示例源码可从我的Gitee的开源仓库获取

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

推荐阅读更多精彩内容