在SpringBoot2中使用Apache Shiro实现登录身份验证和访问权限控制

作为一个Apache Shiro小白,最近跟着“纯洁的微笑”和“冷豪”等的博客,学习了一下Apache Shiro。将自己的一些简单理解记录下来,希望对你有所帮助。

Apache Shiro在我工作项目中主要用于登陆身份验证访问权限控制。工作项目用的是SpringMVC框架,最近学习SpringBoot2,我就在SpringBoot2中来验证一下Apache Shiro。以下Apache Shiro简称Shiro。

一、Shiro登陆架构

下面是Shiro的用户登陆架构图,我们根据箭头来看一下流程。

image.png

1、Token:使用用户的登录信息创建令牌

UsernamePasswordToken token = new UsernamePasswordToken(username, password, true);

我们要先通过用户名和密码,生成一个token,token是一个用户令牌,用于在登陆的时候,Shiro来验证用户是否有合法的身份。

2、Subject:执行登陆动作(login)

Subject subject = SecurityUtils.getSubject(); // 获取Subject单例对象
subject.login(token); // 登陆

再通过Subject来执行登陆操作,将token发送给Security Manager,让他来验证这个token。Subject中文翻译是主题。你可以理解为它是一个用户,是User的抽象概念。

3、Realm:自定义代码实现登陆身份验证和访问权限控制

先来看看Realm,你从上图可以看出,Realm在Shiro方框的外面。图片很形象,因为这一部分恰恰是需要我们自己去实现的。需要我们来设计如何验证登录用户的身份(role),和这个用户是否具有访问某个URL的权限(permission)。前者使用AuthenticationInfo(验证)实现,后者使用AuthorizationInfo(授权)实现。

4、Security Manager:Shiro架构的核心

Security Manager,是Shiro架构的核心,简单来说,它根据我们自定义的Realm,去完成验证和授权工作


如果这部分没有看懂,建议先根据下面的“与SpringBoot2集成”部分,搭建一个demo,在项目中直观体验一下了,再回来看。

二、与SpringBoot2集成

注:以下内容是根据“纯洁的微笑”大神的《springboot整合shiro-登录认证和权限管理》一文,将其中SpringBoot1.5升级到2.1,针对2.1做了相应修改,同时针对一些知识点延伸学习。博文地址:http://www.ityouknow.com/springboot/2017/06/26/springboot-shiro.html

建议你手动搭建一个demo,这样能更深入了解。

pom依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--web核心-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--thymeleaf模板-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--HTML扫描器和标签补偿器,补充thymeleaf对html的严格检验-->
        <dependency>
            <groupId>net.sourceforge.nekohtml</groupId>
            <artifactId>nekohtml</artifactId>
            <version>1.9.22</version>
        </dependency>
        <!--Apache Shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!--使用Spring Data JPA和Hibernate-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--用于MySQL的JDBC Type 4驱动程序-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <!--可使用注解自动生成getter、setter等方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
        </dependency>
</dependencies>

<!--为使用热部署,配置<build></build>-->
<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
</build>

配置文件

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springboot_shiro?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver

  thymeleaf:
    cache: false #禁用模板引擎编译的缓存结果。由热部署来实现,更改代码后,使用Ctrl+F9(IDEA)更新
    mode: LEGACYHTML5 #避免thymeleaf对html文件的严格校验(如检查标签必须对称等)
  
  #使用jpa技术,运行实体代码自动生成数据表
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
        hibernate:
          dialect: org.hibernate.dialect.MySQL5Dialect

server:
  port: 9090

数据库设计

使用基于角色的访问控制(Role-Based Access Control)---RBAC 来实现数据库设计,用户依赖角色,角色依赖权限。这样设计结构清晰,管理方便。建立三张表:user_info,sys_role,sys_permission。使用sys_user_role关联用户和角色,使用sys_role_permission关联角色和权限,不使用外键。

使用jpa技术,运行实体代码自动生成数据表。

用户信息实体。@Getter、@Setter注解用于提供读写属性。因为有getCredentialsSalt(),所以不使用@Data注解。

@Entity
@Getter
@Setter
public class UserInfo implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)//GenerationType.IDENTITY避免生成hibernate_sequence表
    private Integer uid;
    @Column(unique = true)
    private String username;//帐号
    private String name;//名称(昵称或者真实姓名,不同系统不同定义)
    private String password; //密码;
    private String salt;//加密密码的盐
    private byte state;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.

    @ManyToMany(fetch = FetchType.EAGER)//立即从数据库中进行加载数据;
    @JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "uid")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
    private List<SysRole> roleList;// 一个用户具有多个角色

    /**
     * 密码盐,重新对盐重新进行了定义,用户名+salt,这样就更加不容易被破解
     *
     * @return
     */
    public String getCredentialsSalt() {
        return this.username + this.salt;
    }
}

角色实体。使用@Data注解,为类提供读写属性, 此外还提供了 equals()、hashCode()、toString() 方法。上面用户信息实体和角色实体会根据@JoinTable注解生成sys_user_role表。

@Entity
@Data
public class SysRole {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id; // 编号
    private String role; // 角色标识程序中判断使用,如"admin",这个是唯一的:
    private String description; // 角色描述,UI界面显示使用
    private Boolean available = Boolean.FALSE; // 是否可用,如果不可用将不会添加给用户

    // 用户 - 角色关系定义;
    @ManyToMany
    @JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
    private List<UserInfo> userInfos;// 一个角色对应多个用户
    
    //角色 -- 权限关系:多对多关系;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "permissionId")})
    private List<SysPermission> permissions;
}

权限实体。同理,角色实体和权限实体,通过@JoinTable注解生成sys_role_permission表

@Entity
@Data
public class SysPermission {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;//主键.
    private String name;//名称.
    @Column(columnDefinition = "enum('menu','button')")
    private String resourceType;//资源类型,[menu|button]
    private String url;//资源路径.
    private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
    private Long parentId; //父编号
    private String parentIds; //父编号列表
    private Boolean available = Boolean.FALSE;

    @ManyToMany
    @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "permissionId")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
    private List<SysRole> roles;
}

数据库数据:

INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'用户管理',0,'0/','userInfo:view','menu','userInfo/userList');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (2,0,'用户添加',1,'0/1','userInfo:add','button','userInfo/userAdd');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (3,0,'用户删除',1,'0/1','userInfo:del','button','userInfo/userDel');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (1,0,'管理员','admin');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (2,0,'VIP会员','vip');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (3,1,'test','test');
INSERT INTO `sys_role_permission` VALUES ('1', '1');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3,2);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);

三、配置Shiro

Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServletqu去实现。

Filter和Interceptor的区别:

Filter是过滤器,Interceptor是拦截器。前者基于回调函数实现,必须依靠容器支持。因为需要容器装配好整条FilterChain并逐个调用。后者基于代理实现,属于AOP的范畴。

在Shrio中实现登陆身份验证和访问权限控制有三种方式:

  • 1、完全使用注解来实现登陆身份验证和访问权限控制
  • 2、完全使用URL配置来实现登陆身份验证和访问权限控制
  • 3、使用URL配置来实现登陆身份验证、使用注解来实现访问权限控制

第3种方式最灵活,所以用第三种。

1、使用URL配置来实现登陆身份验证

要实现当用户在浏览器地址访问项目URL时,Shiro会拦截所有的请求,再根据配置的ShrioFilter过滤器来进行下一步操作。原理:Spring容器会将所有的Filter交给ShiroFilter管理。

@Configuration
public class ShiroConfig {
   
    @Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 以下过滤器按顺序判断
        // 配置不会被拦截的链接,一般是排除前端文件(anon:指定的url可以匿名访问)
//        filterChainDefinitionMap.put("/static/**", "anon");
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");
        //authc:所有url都必须认证通过才可以访问;
        filterChainDefinitionMap.put("/**", "authc");

        //当项目访问其他没有通过认证的URL时,会默认跳转到/login,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //当用户访问没有权限的URL时,跳转到未授权界面
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
    
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }
    
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
    }
}

authc更深层次含义:指定url需要form表单登录,默认会从请求中获取username、password等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径

在Realm中实现AuthenticationInfo(登陆身份验证)

doGetAuthenticationInfo():用于验证token的User是否具有合法的身份,即检验账号密码是否正确,每次用户登录的时候都会调用。

public class MyShiroRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取登录的用户名
        String username = (String) authenticationToken.getPrincipal();
        //根据用户名在数据库中查找此用户
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        UserInfo userInfo = userService.findByUsername(username);
        if (userInfo == null) {
            return null;
        }
        //根据salt来验证token中的密码是否跟从数据库查找的密码匹配,匹配则登录成功。getName()设置当前Realm的唯一名称,可自定义
        return new SimpleAuthenticationInfo(
                userInfo,
                userInfo.getPassword(),
                ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//盐
                getName());
    }
}

验证密码原理

先了解两个算法,散列算法与加密算法。

两者都是将一个Object变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。

在这里,我们将用户的密码使用散列算法(MD5)加密后保存到数据库。加密的时候就使用了salt,salt中文翻译是盐,你可以将他看成一个钥匙。

因为散列算法加密是单项的,不能还原。那我们如何来验证密码呢,这时候也需要使用salt。我们将token中的明文密码,采用生成密文密码时一样的方式,通过salt再加密一次,对比两个加密后的密码。最后的验证是SimpleAuthenticationInfo去实现的。

如何散列加密

//newPassword(密文密码):d3c59d25033dbf980d29554025c23a75
String newPassword = new SimpleHash("MD5",//散列算法:这里使用MD5算法
        "123456",//明文密码
        ByteSource.Util.bytes("admin8d78869f470951332959580424d4bf4f"),//salt:用户名 + salt
        2//散列的次数,相当于MD5(MD5(**))
).toHex();

//生成一个32位数的salt
byte[] saltByte = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(saltByte);
String salt = Hex.encodeToString((saltByte));   

如何验证密码

配置hashedCredentialsMatcher(凭证匹配器),让SimpleAuthorizationInfo知道如何验证密码:

@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法
    hashedCredentialsMatcher.setHashIterations(2);//散列的次数
    return hashedCredentialsMatcher;
}

2、使用注解来实现访问权限控制

@Controller
@RequestMapping("/userInfo")
public class UserInfoController {
    /**
     * 用户查询;
     * @return
     */
    @RequestMapping("/userList")
    @RequiresPermissions("userInfo:view")//访问的权限
    public String userList(){
        return "userInfo";
    }

    /**
     * 用户添加;
     * @return
     */
    @RequestMapping("/userAdd")
    @RequiresPermissions("userInfo:add")//新增的权限
    public String userAdd(){
        return "userInfoAdd";
    }

    /**
     * 用户删除;
     * @return
     */
    @RequestMapping("/userDel")
    @RequiresPermissions("userInfo:del")//删除的权限
    public String userDel(){
        return "userInfoDel";
    }
}

在Relm中实现AuthorizationInfo(访问权限控制)

如果项目只需要Apache Shiro用于登陆验证,那么就不用使用AuthorizationInfo,只需要返回一个null。

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    return null;
}

doGetAuthorizationInfo():当用户访问带有@RequiresPermissions注解的URL时,会调用此方法验证是否有权限访问。

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
        UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //获取当前用户的角色与权限,让simpleAuthorizationInfo去验证
        for (SysRole sysRole : userInfo.getRoleList()) {
            simpleAuthorizationInfo.addRole(sysRole.getRole());
            for (SysPermission sysPermission : sysRole.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(sysPermission.getPermission());
            }
        }
        return simpleAuthorizationInfo;
}

代码开启注解

使用Shrio注解,需要在ShrioConfig中使用AuthorizationAttributeSourceAdvisor开启

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
}

自定义异常处理

当没有访问权限时,会抛出异常,需要自定义异常处理,将没有权限的异常重定向到403页面

@Bean
    public SimpleMappingExceptionResolver
    createSimpleMappingExceptionResolver() {
        System.out.println("自定义异常处理");
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        mappings.setProperty("UnauthorizedException", "403");//授权异常处理
        resolver.setExceptionMappings(mappings);  // None by default
        resolver.setDefaultErrorView("error");    // No default
        resolver.setExceptionAttribute("ex");     // Default is "exception"
        return resolver;
    }

同理,可以使用@RequiresRoles("admin")注解来验证角色(身份)

在前端页面使用Shiro标签时也会触发权限控制,请看:https://www.jianshu.com/p/6786ddf54582

其他两种验证和授权请看:https://juejin.im/entry/5ad95ef26fb9a07a9f01185a

也可直接编代码测试:

Boolean isPermitted = SecurityUtils.getSubject().isPermitted("***");//是否有什么权限
Boolean hasRole = SecurityUtils.getSubject().hasRole("***");//是否有什么角色

登陆实现

前端登陆页面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
错误信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
    <p>账号:<input type="text" name="username" value="admin"/></p>
    <p>密码:<input type="text" name="password" value="123456"/></p>
    <p><input type="submit" value="登录"/></p>
</form>
</body>
</html>

登陆接口:

@Controller
public class LoginController {

    @RequestMapping("/login")
    public String toLogin(HttpServletRequest request, Map<String, Object> map) {
        System.out.println("HomeController.login()");
        // 登录失败从request中获取shiro处理的异常信息。
        // shiroLoginFailure:就是shiro异常类的全类名.
        String exception = (String) request.getAttribute("shiroLoginFailure");
        System.out.println("exception=" + exception);
        String msg = "";
        if (exception != null) {
            if (UnknownAccountException.class.getName().equals(exception)) {
                System.out.println("UnknownAccountException -- > 账号不存在:");
                msg = "UnknownAccountException -- > 账号不存在:";
            } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                System.out.println("IncorrectCredentialsException -- > 密码不正确:");
                msg = "IncorrectCredentialsException -- > 密码不正确:";
            } else if ("kaptchaValidateFailed".equals(exception)) {
                System.out.println("kaptchaValidateFailed -- > 验证码错误");
                msg = "kaptchaValidateFailed -- > 验证码错误";
            } else {
                msg = "else >> "+exception;
                System.out.println("else -- >" + exception);
            }
        }
        map.put("msg", msg);
        return "login";
    }

    @RequestMapping({"/","/index"})
    public String index(){
        return "index";
    }
}

你可能发现了,登陆没有用文章开头的Subject执行登陆动作,而是直接使用action=""的表单登录。
为什么action=""呢,这是因为设置了"/**", "authc",当用户没有登陆时,所有url(/**)都会被重定向到/login,而action=""或者"/login"将不被拦截,doGetAuthenticationInfo()验证表单中的账号密码。

使用Subject执行登陆动作

那如何使用Subject执行登陆动作呢,需要使用user过滤器。当没有登录时,user跟authc一样,会拦截所有的url。

//user:需要已登录或“记住我”的用户才能访问;
filterChainDefinitionMap.put("/**", "user");

当使用Post请求的/login登陆时,将使用Subject执行登陆动作,doGetAuthenticationInfo()方法验证

<form action="/login" method="post">
    <p>账号:<input type="text" name="username" value="admin"/></p>
    <p>密码:<input type="text" name="password" value="123456"/></p>
    <p><input type="submit" value="登录"/></p>
</form>

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(@RequestParam(value = "username") String userName,
                    @RequestParam(value = "password") String password) {
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
        } catch (AuthenticationException e) {
            e.printStackTrace();
        }
        return "index";
}

项目源码

其余未贴出代码请查看项目源码https://github.com/DeppWang/SpringBoot-Demo

总结

通过ShiroFilter配置的过滤器,Shiro拦截所有未过滤的url。如果未登陆,跳转到登陆页(loginUrl)。直接使用表单,或者使用subject.login()登陆时。在doGetAuthenticationInfo()中验证。验证通过,跳转到成功页(successUrl)。

当访问需要访问权限的url时,从数据库查询当前用户的权限,最后交给Shiro框架去验证。在doGetAuthorizationInfo()中实现。

参考资料

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

推荐阅读更多精彩内容