SpringBoot 2.2.5 配置AOP,实现API接口请求体及响应体自动加解密的功能

说明

  1. Spring的AOP的存在目的是为了解耦。AOP可以让一组类共享相同的行为。在OOP中只能继承和实现接口,且类继承只能单继承,阻碍更多行为添加到一组类上,AOP弥补了OOP的不足。
  2. 还有就是为了清晰的逻辑,让业务逻辑关注业务本身,不用去关心其它的事情,比如事务。

专业术语简单解释

  1. 通知(有的地方叫增强)(Advice):需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用
  2. 连接点(Join point):就是spring中允许使用通知的地方,基本上每个方法前后抛异常时都可以是连接点
  3. 切点(Poincut):其实就是筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。如果说通知定义了切面的动作或者执行时机的话,切点则定义了执行的地点
  4. 切面(Aspect):其实就是通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行
  5. 引入(Introduction):在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去
  6. 目标(target):被通知的对象。也就是需要加入额外代码的对象,也就是真正的业务逻辑被组织织入切面。
  7. 织入(Weaving):把切面加入程序代码的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入:

完整代码地址在结尾!!

第一步,在pom.xml加入依赖,如下

<!-- SpringBoot-AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步,编写application.yml配置文件,如下

server:
  port: 8081

spring:
  application:
    name: aop-demo-server

http:
  check:
    key:
      aes:
        request: w@sd8dlm # 解密请求体默认key
        response: ems&koq3 # 加密响应体默认key
    request:
      timeout: 10 # 请求时间超时时间,单位秒

第三步,创建KeyConfig配置文件,如下

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component
public class KeyConfig {

    /**
     * 解密请求体默认key
     */
    @Value("${http.check.key.aes.request}")
    private String keyAesRequest;

    /**
     * 加密响应体默认key
     */
    @Value("${http.check.key.aes.response}")
    private String KeyAesResponse;

    /**
     * 请求时间超时时间,单位秒
     */
    @Value("${http.check.request.timeout}")
    private String timeout;

}

第四步,创建工具类,JsonUtils,AESUtil,如下

JsonUtils

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * JsonUtils
 *
 * @author luoyu
 * @date 2018/10/08 19:13
 * @description Json工具类,依赖 jackson
 */
@Slf4j
public class JsonUtils {

    private  static ObjectMapper objectMapper = null;

    static {
        objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }

    /**
     * 对象转换成json
     * @param obj
     * @param <T>
     * @return
     */
    public static <T>String objectToJson(T obj){
        if(obj == null){
            return null;
        }
        try {
            return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.error("Parse Object to Json error",e);
            return null;
        }
    }

    /**
     * 将json转换成对象Class
     * @param src
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T>T jsonToObject(String src,Class<T> clazz){
        if(StringUtils.isEmpty(src) || clazz == null){
            return null;
        }
        try {
            return clazz.equals(String.class) ? (T) src : objectMapper.readValue(src,clazz);
        } catch (Exception e) {
            log.warn("Parse Json to Object error",e);
            return null;
        }
    }

    /**
     * 字符串转换为 Map<String, Object>
     *
     * @param src
     * @return
     * @throws Exception
     */
    public static <T> Map<String, Object> jsonToMap(String src) {
        if(StringUtils.isEmpty(src)){
            return null;
        }
        try {
            return objectMapper.readValue(src, Map.class);
        } catch (Exception e) {
            log.warn("Parse Json to Map error",e);
            return null;
        }

    }

    public static <T> List<T> jsonToList(String jsonArrayStr, Class<T> clazz) {
        try{
            JavaType  javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class, clazz);
            return (List<T>) objectMapper.readValue(jsonArrayStr, javaType);
        }catch (Exception e) {
            log.warn("Parse Json to Map error",e);
            return null;
        }

    }

}

AESUtil

import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

/**
 * @version 1.0
 * @author luoyu
 * @date 2018-05-09
 * @description AES工具类
 */
public class AESUtil {

    private final static String KEY_ALGORITHM = "AES";

    //默认的加密算法
    private final static String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    /**
     * @Author: jinhaoxun
     * @Description: AES 加密操作
     * @param content 待加密内容
     * @param key 加密密钥
     * @Date: 2020/4/2 上午12:46
     * @Return: javax.crypto.spec.SecretKeySpec 返回Base64转码后的加密数据
     * @Throws:
     */
    public static String encrypt(String content, String key) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);// 创建密码器
            byte[] byteContent = content.getBytes("utf-8");
            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));// 初始化为加密模式的密码器
            byte[] result = cipher.doFinal(byteContent);// 加密
            return new String(Base64Utils.encode(result));//通过Base64转码返回
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * @Author: jinhaoxun
     * @Description: AES 解密操作
     * @param content
     * @param key
     * @Date: 2020/4/2 上午12:46
     * @Return: javax.crypto.spec.SecretKeySpec
     * @Throws:
     */
    public static String decrypt(String content, String key) {
        try {
            //实例化
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            //使用密钥初始化,设置为解密模式
            cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));
            //执行操作
            byte[] result = cipher.doFinal(Base64Utils.decode(content.getBytes()));
            return new String(result, "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * @Author: jinhaoxun
     * @Description: 生成加密秘钥
     * @param key
     * @Date: 2020/4/2 上午12:46
     * @Return: javax.crypto.spec.SecretKeySpec
     * @Throws:
     */
    private static SecretKeySpec getSecretKey(final String key) {
        //返回生成指定算法密钥生成器的 KeyGenerator 对象
        KeyGenerator kg = null;
        try {
            kg = KeyGenerator.getInstance(KEY_ALGORITHM);
            // 解决操作系统内部状态不一致问题(部分liunx不指定类型,无法解密)
            SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
            secureRandom.setSeed(key.getBytes());
            kg.init(128, secureRandom);

//            kg.init(128, new SecureRandom(key.getBytes()));
            //生成一个密钥
            SecretKey secretKey = kg.generateKey();
            return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);// 转换为AES专用密钥
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

}

第五步,创建实体类,HttpRequest,HttpResponse,TestRequest,TestResponse,如下

HttpRequest

import lombok.Data;

import javax.validation.Valid;

/**
* @Description: Http的请求的大对象
* @author jinhaoxun
* @date 2019年12月29日 下午8:16:52
 */
@Data
public class HttpRequest<T>{
    /**
     * 客户端的版本
     */
    private String version;
    /**
     * 客户端的渠道
     */
    private String channel;
    /**
     * 客户端发起请求的时间
     */
    private long time;
    /**
     * 请求的数据对象
     */
    @Valid
    private T data;
    /**
     * 请求的密文,如果该接口需要加密上送,
     * 则将sdt的密文反序化到data,
     * sdt和action至少有一个为空
     */
    private String sdt;

}

HttpResponse

import lombok.Data;

import java.time.LocalDateTime;
import java.time.ZoneOffset;

/**
 * @Description: Http的响应大对象
 * @author jinhaoxun
 * @date 2019年12月29日 下午8:15:39
 */
@Data
public class HttpResponse<T>{
    /**
     * 响应码
     */
    private Integer code;
    /**
     * 响应时间
     */
    private long time;
    /**
     * 响应的信息(一般为错误信息)
     */
    private String msg;
    /**
     * 响应数据(一般为ActionResponse的子类)
     */
    private T data;
    /**
     * 响应的密文,如果该接口需要加密返回,
     * 则将data的密文绑定到该字段上,
     * srs和data至少有一个为空
     */
    private String srs;
    
    /**
     * 私有化默认构造器
     */
    private HttpResponse() {
    }
    
    private HttpResponse(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
        this.time = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();;
    }

    /**
    * @Description: 构建一个响应
    * @author jinhaoxun
    * @date 2019年1月2日 下午2:09:24 
    * @param code
    * @param msg
    * @param data
    * @return
     */
    public static <T> HttpResponse<T> build(Integer code, String msg, T data){
        return new HttpResponse<T>(code, msg, data);
    }

}

TestRequest

import lombok.Data;

/**
 * @Description:
 * @Author: jinhaoxun
 * @Date: 2020/7/10 10:36 上午
 * @Version: 1.0.0
 */
@Data
public class TestRequest{

    private int id;
    private String name;
    private String sex;

}

TestResponse

import lombok.Data;

/**
 * @Description:
 * @Author: jinhaoxun
 * @Date: 2020/7/10 10:36 上午
 * @Version: 1.0.0
 */
@Data
public class TestResponse{

    private int id;
    private String name;
    private String sex;

}

第六步,创建aop类,HttpCheck,HttpCheckAspact,如下

HttpCheck

import java.lang.annotation.*;

/**
 *
* @Description: http请求的参数校验
* @author luoyu
* @date 2019年1月9日 下午6:53:41
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HttpCheck {

    /**
     * 是否需要解密(默认需要解密)
     */
    boolean isDecrypt() default true;

    /**
     * 解密的key,只有当isDecrypt=true
     * 才会检测该字段,并且传入为空时,
     * 用系统预先设置的key进行解密。
     */
    String decryptKey() default "";

    /**
     * 解密,系统统一加密反序列化的类
     */
    Class<?>  dataType();

    /**
     * 是否需要加密返回(默认加密返回)
     */
    boolean isEncrypt() default true;

    /**
     * 加密的key,只有当isEncrypt=true
     * 才会检测该字段,并且传入为空时,
     * 用系统预先设置的key进行加密返回
     */
    String encryptKey() default "";

    /**
     * 是否需要检测超时时间(默认需要)
     */
    boolean isTimeout() default true;

    /**
     * 超时时间,只有当isTimeout=true
     * 才会检测该字段,并且传入为空时,
     * 用系统预先设置的timeout进行加密返回
     */
    String timeout() default "";

}

HttpCheckAspact

import com.luoyu.aop.config.KeyConfig;
import com.luoyu.aop.entity.http.HttpRequest;
import com.luoyu.aop.entity.http.HttpResponse;
import com.luoyu.aop.entity.response.TestResponse;
import com.luoyu.aop.util.AESUtil;
import com.luoyu.aop.util.JsonUtils;
import com.oracle.tools.packager.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

/**
 * @Auther: luoyu
 * @Date: 2019/1/22 16:28
 * @Description:
 */
@Aspect
@Component
public class HttpCheckAspact {

    @Autowired
    private KeyConfig keyConfig;

    @Pointcut("@annotation(com.luoyu.aop.aop.HttpCheck)")
    public void pointcut() {
    }

    /**
     *  前置处理
     * @param joinPoint
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) throws Exception {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        HttpCheck annotation = method.getAnnotation(HttpCheck.class);
        // 获取HttpRequest对象
        Object[] args = joinPoint.getArgs();
        HttpRequest httpRequest = null;
        if(args.length > 0){
            for(Object arg: args){
                if(arg instanceof HttpRequest){
                    httpRequest = (HttpRequest)arg;
                }
            }
        }else {
            throw new Exception("请求参数错误!");
        }

        // 是否需要检测超时时间
        if (annotation.isTimeout()){
            // 获取超时时间
            String timeout = StringUtils.isEmpty(annotation.timeout()) ? keyConfig.getTimeout(): annotation.timeout();
            if(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli() < httpRequest.getTime()){
                throw new Exception("请求时间错误!");
            }
            if(LocalDateTime.now().minusSeconds(Integer.parseInt(timeout)).toInstant(ZoneOffset.of("+8")).toEpochMilli() > httpRequest.getTime()){
                throw new Exception("请求已超时!");
            }
            Log.info("检测超时时间成功!");
        }

        // 是否需要进行解密
        if(annotation.isDecrypt()){
            // 获取需要解密的key
            String dectyptKey = StringUtils.isEmpty(annotation.decryptKey()) ? keyConfig.getKeyAesRequest(): annotation.decryptKey();

            String sdt = httpRequest.getSdt();
            if(StringUtils.isEmpty(sdt)){
                throw new Exception("sdt不能为空!");
            }
            String context = AESUtil.decrypt(sdt, dectyptKey);
            if(StringUtils.isEmpty(context)){
                throw new Exception("sdt解密出错!");
            }
            Log.info("解密成功!");
            // 设置解密后的data
            httpRequest.setData(JsonUtils.jsonToObject(context, annotation.dataType()));
        }

    }

    @AfterReturning(value = "pointcut()", returning = "response")
    public void doAfterReturning(JoinPoint joinPoint, Object response) throws Exception {
        HttpResponse httpResponse = (HttpResponse) response;
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        HttpCheck annotation = method.getAnnotation(HttpCheck.class);
        if(annotation.isEncrypt()){
            TestResponse body =  (TestResponse) httpResponse.getData();
            // 进行响应加密
            if (body != null) {
                String encrypyKey = StringUtils.isEmpty(annotation.encryptKey())? keyConfig.getKeyAesResponse() : annotation.encryptKey();
                // 设置加密后的srs
                httpResponse.setSrs(AESUtil.encrypt(JsonUtils.objectToJson(body), encrypyKey));
                Log.info("加密成功!");
                httpResponse.setData(null);
            }
        }
    }

}

第七步,创建服务类,TestService,TestServiceImpl,如下

TestService

import com.luoyu.aop.entity.request.TestRequest;
import com.luoyu.aop.entity.response.TestResponse;

/**
 * @Description:
 * @Author: luoyu
 * @Date: 2020/7/10 10:31 上午
 * @Version: 1.0.0
 */
public interface TestService {

    TestResponse get(TestRequest testRequest) throws Exception;

}

TestServiceImpl

import com.luoyu.aop.service.TestService;
import com.luoyu.aop.entity.request.TestRequest;
import com.luoyu.aop.entity.response.TestResponse;
import org.springframework.stereotype.Service;

/**
 * @Description:
 * @Author: luoyu
 * @Date: 2020/7/10 10:32 上午
 * @Version: 1.0.0
 */
@Service
public class TestServiceImpl implements TestService {

    @Override
    public TestResponse get(TestRequest testRequest) throws Exception {
        TestResponse testResponse = new TestResponse();
        testResponse.setId(testRequest.getId());
        testResponse.setName(testRequest.getName());
        testResponse.setSex(testRequest.getSex());
        return testResponse;
    }

}

第八步,创建TestController类,如下

import com.luoyu.aop.aop.HttpCheck;
import com.luoyu.aop.service.TestService;
import com.luoyu.aop.entity.http.HttpRequest;
import com.luoyu.aop.entity.http.HttpResponse;
import com.luoyu.aop.entity.request.TestRequest;
import com.luoyu.aop.entity.response.TestResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @Description:
 * @Author: luoyu
 * @Date: 2020/7/10 10:31 上午
 * @Version: 1.0.0
 */
@RestController
@RequestMapping("/test")
public class TestController {

    @Resource
    TestService testService;

    /**
     * @author luoyu
     * @description 测试接口1
     */
    @PostMapping(value = "/test1", produces = "application/json; charset=UTF-8")
    @HttpCheck(dataType = TestRequest.class)
    public HttpResponse<TestResponse> Test1(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
        TestResponse testResponse = testService.get(httpRequest.getData());
        return HttpResponse.build(200, "成功!", testResponse);
    }

    /**
     * @author luoyu
     * @description 测试接口2
     */
    @PostMapping(value = "/test2", produces = "application/json; charset=UTF-8")
    @HttpCheck(dataType = TestRequest.class, isTimeout = false)
    public HttpResponse<TestResponse> Test2(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
        TestResponse testResponse = testService.get(httpRequest.getData());
        return HttpResponse.build(200, "成功!", testResponse);
    }

    /**
     * @author luoyu
     * @description 测试接口3
     */
    @PostMapping(value = "/test3", produces = "application/json; charset=UTF-8")
    @HttpCheck(dataType = TestRequest.class, isDecrypt = false, isTimeout = false)
    public HttpResponse<TestResponse> Test3(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
        TestResponse testResponse = testService.get(httpRequest.getData());
        return HttpResponse.build(200, "成功!", testResponse);
    }

    /**
     * @author luoyu
     * @description 测试接口4
     */
    @PostMapping(value = "/test4", produces = "application/json; charset=UTF-8")
    @HttpCheck(dataType = TestRequest.class, isEncrypt = false, isTimeout = false)
    public HttpResponse<TestResponse> Test4(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
        TestResponse testResponse = testService.get(httpRequest.getData());
        return HttpResponse.build(200, "成功!", testResponse);
    }

    /**
     * @author luoyu
     * @description 测试接口5
     */
    @PostMapping(value = "/test5", produces = "application/json; charset=UTF-8")
    @HttpCheck(dataType = TestRequest.class, isDecrypt = false, isEncrypt = false, isTimeout = false)
    public HttpResponse<TestResponse> Test5(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
        TestResponse testResponse = testService.get(httpRequest.getData());
        return HttpResponse.build(200, "成功!", testResponse);
    }

}

第九步,创建单元测试类,AopApplicationTests,对简单实体参数进行加解密,如下

import com.luoyu.aop.config.KeyConfig;
import com.luoyu.aop.entity.response.TestResponse;
import com.luoyu.aop.util.AESUtil;
import com.luoyu.aop.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
// 获取启动类,加载配置,确定装载 Spring 程序的装载方法,它回去寻找 主配置启动类(被 @SpringBootApplication 注解的)
@SpringBootTest
class AopApplicationTests {

    @Autowired
    private KeyConfig keyConfig;

    @Test
    void AESEncryptRequestTest() throws Exception {
        TestResponse testResponse = new TestResponse();
        testResponse.setId(1);
        testResponse.setName("test");
        testResponse.setSex("男");

        String encrypt = AESUtil.encrypt(JsonUtils.objectToJson(testResponse), keyConfig.getKeyAesRequest());
        log.info(encrypt);
    }

    @Test
    void AESDecryptRequestTest() throws Exception {
        String str = "AdChWsCSrehSLAJVUalBseXKZ7BVQ0RS5hd5EryE+hE2GZ+upLPM1hR2kgCwseeF";

        String decrypt = AESUtil.decrypt(str, keyConfig.getKeyAesRequest());
        log.info(decrypt);
    }

    @Test
    void AESEncryptResponseTest() throws Exception {
        TestResponse testResponse = new TestResponse();
        testResponse.setId(1);
        testResponse.setName("test");
        testResponse.setSex("男");

        String encrypt = AESUtil.encrypt(JsonUtils.objectToJson(testResponse), keyConfig.getKeyAesResponse());
        log.info(encrypt);
    }

    @Test
    void AESDecryptResponseTest() throws Exception {
        String str = "ReXg0r2PHewqdtD/ucSUU05UtLcbNSaPiTWzQj6EHGqtDrokVclzeTMlow5OPthC";

        String decrypt = AESUtil.decrypt(str, keyConfig.getKeyAesResponse());
        log.info(decrypt);
    }


    @BeforeEach
    void testBefore(){
        log.info("测试开始!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    }

    @AfterEach
    void testAfter(){
        log.info("测试结束!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    }

}

第十步,验证流程如下

  1. 使用AopApplicationTests测试类,对简单实体参数进行加解密
  2. 然后用加密后的参数,使用postman工具调用api接口
  3. 调用TestController不同的api接口,通过出入参数的不同,验证是否实现功能

完整代码地址:https://github.com/Jinhx128/springboot-demo

注:此工程包含多个module,本文所用代码均在aop-demo模块下

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