Spring Boot之整合Spring Security: 授权管理

前言

笔者学习Spring Boot有一段时间了,附上Spring Boot系列学习文章,大家有兴趣可以参考参考:

  1. 5分钟入手Spring Boot;
  2. Spring Boot数据库交互之Spring Data JPA;
  3. Spring Boot数据库交互之Mybatis;
  4. Spring Boot视图技术;
  5. Spring Boot之整合Swagger;
  6. Spring Boot之junit单元测试踩坑;
  7. 如何在Spring Boot中使用TestNG;
  8. Spring Boot之整合logback日志;
  9. Spring Boot之整合Spring Batch:批处理与任务调度;
  10. Spring Boot之整合Spring Security: 访问认证;

在上一篇文章Spring Boot之整合Spring Security:访问认证中,我们一起学习了Spring Security的访问认证实现,旨在探索如何用Spring Security进行访问认证控制,简单的说就是:

  • 未登录状态下,站点的所有访问均跳转到登录页面,包括API;

而这样的操作或设置远不能代表真实场景,一般我们会面临以下问题:

1. 未登录状态下,访问API应返回HTTP 状码401,并伴随提示性response body;
2. 不同用户需要不同的访问权限,即权限管理;

今天我们就来探索如何实现这2个需求!

项目代码仍用已上传的Git Hub仓库,欢迎取阅:

整体步骤

  1. 准备不同角色的用户;
  2. 准备测试接口;
  3. 美化登录页面;
  4. 授权管理配置;
  5. 验证授权效果;

1. 准备不同角色的用户;

1). 规范化角色;

在上一篇文章Spring Boot之整合Spring Security:访问认证,我们在多处使用角色信息:

.roles("admin")
...
.roles("user")
...
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");
...

像这种多处使用的数据,应该做个集中管理与限制,因此,我们在项目中创建constant包,创建一个枚举类:UserTypeEnum,代码如:

package com.github.dylanz666.constant;

/**
 * @author : dylanz
 * @since : 09/07/2020
 */
public enum UserTypeEnum {
    ADMIN,
    USER
}

然后把所有角色进行重构替换,这样我们将角色进行集中管理与限制,更为严谨;

  • User实体类增加userType;
package com.github.dylanz666.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @author : dylanz
 * @since : 08/31/2020
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String username;
    private String password;
    private String userType;
}
  • 创建用户时存储角色信息,查询时也查询出角色信息;
package com.github.dylanz666.service;

import com.github.dylanz666.constant.UserTypeEnum;
import com.github.dylanz666.domain.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author : dylanz
 * @since : 08/31/2020
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private UserDetailsImpl userService;
    @Autowired
    private UserDetails userDetails;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //Spring Security要求必须加密密码
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        //模拟从数据库中取出用户信息,使用的sql如: SELECT * FROM USER WHERE USER_NAME='cherrys'
        List<User> userList = new ArrayList<>();
        User firstUser = new User();
        firstUser.setUsername("cherrys");
        firstUser.setPassword(passwordEncoder.encode("123"));
        firstUser.setUserType(UserTypeEnum.USER.toString());
        userList.add(firstUser);
        User secondUser = new User();
        secondUser.setUsername("randyh");
        secondUser.setPassword(passwordEncoder.encode("456"));
        secondUser.setUserType(UserTypeEnum.USER.toString());
        userList.add(secondUser);

        List<User> mappedUsers = userList.stream().filter(s -> s.getUsername().equals(username)).collect(Collectors.toList());

        //判断用户是否存在
        User user;
        if (CollectionUtils.isEmpty(mappedUsers)) {
            logger.info(String.format("The user %s is not found !", username));
            throw new UsernameNotFoundException(String.format("The user %s is not found !", username));
        }
        user = mappedUsers.get(0);
        return new UserDetailsImpl(user);
    }
}
  • 使用角色信息时,均在限定范围内:
.roles(UserTypeEnum.ADMIN.toString())
...
.roles(UserTypeEnum.UAER.toString())
...
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.currentUser.getUserType());
...

2. 准备测试接口;

我们准备4种接口,用于Demo授权管理:

  • 任何角色登录均可访问;
  • 无需登录即可访问;
  • ADMIN角色登录方可访问;
  • USER及比USER权限大的角色登录方可访问;
package com.github.dylanz666.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@RestController
public class HelloController {
    @GetMapping("/hello")//任何角色登录均可访问;
    public String sayHello() {
        return "Hello!";
    }

    @GetMapping("/ping")//无需登录即可访问;
    public String ping() {
        return "Success!";
    }

    @GetMapping("/admin/hello")//ADMIN角色登录方可访问;
    public String adminHello() {
        return "Hello admin!";
    }

    @GetMapping("/user/hello")//USER及比USER权限大的角色登录方可访问;
    public String userHello() {
        return "Hello user!";
    }
}

3. 美化登录页面;

在上一期文章中,我们使用了自定义的登录页面,但样子实在丑,有同学也许想看下,我们如何自己做个美丽的登录页面,因此我也稍微美化了一下:

1). 在resources文件夹下创建static文件夹,用于放置静态资源,如图片、CSS文件、js文件等,我们用于放一张登录背景图:

登录背景图

2). 更新resources/templates/login.html模板文件:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Spring Security Example</title>
</head>
<body class="body">
<div class="main">
    <div class="welcome">Welcome</div>
    <hr/>
    <form th:action="@{/login}" method="post">
        <div>
            <input type="text" name="username" placeholder="username" class="input">
        </div>
        <div>
            <input type="password" name="password" placeholder="password" class="input">
        </div>
        <div th:style="'vertical-align:middle'">
            <button class="button">Sign In</button>
        </div>
    </form>
</div>
</body>
<style>
    .body {
        background-image: url("/20200907.jpg");
        background-repeat: no-repeat;
        background-position: fixed;
        background-size: cover
    }

    .welcome {
        font-size:36px;color: white;
    }

    .main {
        border:5px solid white;
        border-radius: 5px;
        width: 320px;
        height: 220px;
        margin: 120px auto;
        display: table;
        text-align: center;
        line-height: 40px;
        vertical-align: middle;
        display: table;
    }

    .button {
        margin-top: 10px;
        border: none;
        background-color: #4CAF50;
        color: white;
        padding: 12px 30px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 14px;
        border-radius: 3px;
    }

    .input {
        margin-top: 10px;
        border: 2px solid #a1a1a1;
        background: white;
        width: 200px;
        height: 18px;
        padding: 12px 30px;
        border-radius: 5px;
        padding: 10px 28px;
        text-decoration: none;
        display: inline-block;
        font-size: 14px;
        border-radius: 3px;
    }
</style>
</html>

3). 登录页面效果:


登录页面效果

感觉漂亮多了吧!

4. 授权管理配置;

1). 自定义无权限报错实体类;

在授权之前,我们先自定义一个无权限报错实体类,定义当无权限访问时,告知客户端的信息。在domain包下创建AuthorizationException实体类,代码如下:

package com.github.dylanz666.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @author : dylanz
 * @since : 09/07/2020
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class AuthorizationException implements Serializable {
    private static final long serialVersionUID = 1L;

    private int code;
    private String status;
    private String uri;
    private String message;

    @Override
    public String toString() {
        return "{" +
                "\"code\":\"" + code + "\"," +
                "\"status\":\"" + status + "\"," +
                "\"message\":\"" + message + "\"," +
                "\"uri\":\"" + uri + "\"" +
                "}";
    }
}
2). 开放静态资源访问;

修改config包下的WebMvcConfig类,如:

package com.github.dylanz666.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home.html").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello.html").setViewName("hello");
        registry.addViewController("/login.html").setViewName("login");
    }

    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }
}
3). 授权管理配置;
  • 修改WebSecurityConfig如下:
package com.github.dylanz666.config;

import com.github.dylanz666.constant.UserTypeEnum;
import com.github.dylanz666.domain.AuthorizationException;
import com.github.dylanz666.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private AuthorizationException authorizationException;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/*.jpg", "/*.png", "/*.css", "/*.js");
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .antMatchers("/", "/home.html", "/ping").permitAll()//这3个url不用访问认证
                .antMatchers("/admin/**").hasRole(UserTypeEnum.ADMIN.toString())
                .antMatchers("/user/**").hasRole(UserTypeEnum.USER.toString())
                .anyRequest()
                .authenticated()//其他url都需要访问认证
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面的url
                .loginProcessingUrl("/login")//登录表使用的API
                .permitAll()//login.html和login不需要访问认证
                .and()
                .logout()
                .permitAll()//logout不需要访问认证
                .and()
                .csrf()
                .disable()
                .exceptionHandling()
                .accessDeniedHandler(((httpServletRequest, httpServletResponse, e) -> {
                    httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    httpServletResponse.setContentType("application/json");
                    authorizationException.setCode(HttpServletResponse.SC_FORBIDDEN);
                    authorizationException.setStatus("FAIL");
                    authorizationException.setMessage("FORBIDDEN");
                    authorizationException.setUri(httpServletRequest.getRequestURI());
                    PrintWriter printWriter = httpServletResponse.getWriter();
                    printWriter.write(authorizationException.toString());
                    printWriter.flush();
                    printWriter.close();
                }))
                .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
                    if (httpServletRequest.getRequestURI().equals("/hello.html")) {
                        httpServletResponse.sendRedirect("/login.html");
                        return;
                    }
                    httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    httpServletResponse.setContentType("application/json");
                    authorizationException.setCode(HttpServletResponse.SC_UNAUTHORIZED);
                    authorizationException.setStatus("FAIL");
                    authorizationException.setUri(httpServletRequest.getRequestURI());
                    authorizationException.setMessage("UNAUTHORIZED");
                    PrintWriter printWriter = httpServletResponse.getWriter();
                    printWriter.write(authorizationException.toString());
                    printWriter.flush();
                    printWriter.close();
                });
        httpSecurity.userDetailsService(userDetailsService());
        httpSecurity.userDetailsService(userDetailsService);
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        UserDetails dylanz =
                User.withUsername("dylanz")
                        .password(bCryptPasswordEncoder.encode("666"))
                        .roles(UserTypeEnum.ADMIN.toString())
                        .build();
        UserDetails ritay =
                User.withUsername("ritay")
                        .password(bCryptPasswordEncoder.encode("888"))
                        .roles(UserTypeEnum.USER.toString())
                        .build();
        UserDetails jonathanw =
                User.withUsername("jonathanw")
                        .password(bCryptPasswordEncoder.encode("999"))
                        .roles(UserTypeEnum.USER.toString())
                        .build();
        return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_" + UserTypeEnum.ADMIN.toString() + " > ROLE_" + UserTypeEnum.USER.toString());
        return roleHierarchy;
    }
}

我们来解读一下:

  • "/.jpg", "/.png", "/.css", "/.js" 这几种类型的资源访问,均不需要认证;
  • 对API进行授权,不同API需要不同的角色:
//以admin开头的API,需要ADMIN或更大权限的角色;
.antMatchers("/admin/**").hasRole(UserTypeEnum.ADMIN.toString())
//以user开头的API,需要USER或更大权限的角色;
.antMatchers("/user/**").hasRole(UserTypeEnum.USER.toString())
  • 当权限不足时,我们自定义了权限不足逻辑:
    (1). 访问的资源时,由于权限不足,角色权限不足,则API报403,且API返回我们自定义的无权限报错信息;
    (2). 当用户访问资源时,由于用户未登录,则API报401,且API返回我们自定义的无权限报错信息;
    (3). 当用户访问资源时,权限不足且访问的是/hello.html页面,则重定向到登录页面/login.html,这样避免权限不足访问/hello.html页面时也报401;
                .exceptionHandling()
                .accessDeniedHandler(((httpServletRequest, httpServletResponse, e) -> {                    httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    httpServletResponse.setContentType("application/json");
                    authorizationException.setCode(HttpServletResponse.SC_FORBIDDEN);
                    authorizationException.setStatus("FAIL");
                    authorizationException.setMessage("FORBIDDEN");
                    authorizationException.setUri(httpServletRequest.getRequestURI());
                    PrintWriter printWriter = httpServletResponse.getWriter();
                    printWriter.write(authorizationException.toString());
                    printWriter.flush();
                    printWriter.close();
                }))
                .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
                    if (httpServletRequest.getRequestURI().equals("/hello.html")) {
                        httpServletResponse.sendRedirect("/login.html");
                        return;
                    }
                    httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    httpServletResponse.setContentType("application/json");
                    authorizationException.setCode(HttpServletResponse.SC_UNAUTHORIZED);
                    authorizationException.setStatus("FAIL");
                    authorizationException.setUri(httpServletRequest.getRequestURI());
                    authorizationException.setMessage("UNAUTHORIZED");
                    PrintWriter printWriter = httpServletResponse.getWriter();
                    printWriter.write(authorizationException.toString());
                    printWriter.flush();
                    printWriter.close();
                });
4). 角色继承;

在实际使用场景中,有些角色拥有其他角色的所有权限,这时,如果为每个角色都单独创建完整的权限表,那么有时候会相当冗余。
因此,这时候我们就要用到Spirng Security中的角色继承;
在WebSecurityConfig类中,我写了一个角色继承的例子:

@Bean
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    roleHierarchy.setHierarchy("ROLE_" + UserTypeEnum.ADMIN.toString() + " > ROLE_" + UserTypeEnum.USER.toString());
    return roleHierarchy;
}

即:USER继承于ADMIN角色,USER角色只拥有ADMIN的部分功能,而ADMIN拥有USER角色的所有功能;
注意角色继承的写法,每个角色前要加ROLE_,继承时用 > 符号连接,符号左边权限大,符号右边权限小。

5). 资源多角色访问配置;

假设我们有些/any开头的API,可以给多个角色使用,如SUPERVISOR角色(假设有这个角色)和USER角色,我们可以在WebSecurityConfig中这么配置:

.antMatchers("/any/**").hasAnyRole(UserTypeEnum.SUPERVISOR.toString(), UserTypeEnum.USER.toString())

5. 验证授权效果;

启动项目:


启动项目

开始验证:

1). 访问不用访问认证的API;
访问不用访问认证的API
2). 访问需要任意角色通过访问认证的API;
  • 登录前:


    需任意角色认证API,登录前
  • 登录后:

需任意角色认证API,登录后
3). 访问需要ADMIN角色通过访问认证的API;
  • 登录前:


    需要ADMIN角色认证API,登录前
  • 登录后:


    需要ADMIN角色认证API,登录后
4). 访问需要USER角色通过访问认证的API;
  • 登录前:


    需要USER角色认证API,登录前
  • 登录后:


    需要USER角色认证API,登录后
  • 访问权限外API:(注意,此时的API status code为403,可以从浏览器的Network中查看)


    访问权限外API
5). 访问不存在的API;

我没有额外定制不存在的错误信息或错误页面,默认为:


访问不存在的API

Controller中授权管理

除了上述在WebSecurityConfig统一对API进行授权,我们还可以在项目的Controller中进行授权管理,步骤:

1. WebSecurityConfig类添加注解:@EnableGlobalMethodSecurity(prePostEnabled = true),如:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
2. Controller内增加另外的API,并在API中配置权限,而不是在WebSecurityConfig中配置:
    @GetMapping("/controller/hello")
    @PreAuthorize(value="isAuthenticated()")//任何角色登录均可访问;
    public String controllerAnyHello() {
        return "Hello controller any!";
    }

    @GetMapping("/controller/admin/hello")
    @PreAuthorize("hasRole('ADMIN')")//ADMIN角色登录方可访问;
    public String controllerAdminHello() {
        return "Hello controller admin!";
    }

    @GetMapping("/controller/both/hello")
    @PreAuthorize("hasAnyRole('ADMIN', 'USER')")//ADMIN或USER角色登录方可访问;
    public String controllerBothHello() {
        return "Hello controller both!";
    }

简单分析一下:

1). 使用@EnableGlobalMethodSecurity注解后,Controller中的@PreAuthorize方可生效;
2). @PreAuthorize注解内可以指定权限,如:

  • @PreAuthorize(value="isAuthenticated()"),代表//任何角色登录均可访问;
  • @PreAuthorize("hasRole('ADMIN')"),代表ADMIN角色登录方可访问;
  • @PreAuthorize("hasAnyRole('ADMIN', 'USER')"),可用hasAnyRole为指定的多个角色进行授权;
  • 这种注解方式,hasRole和hasAnyRole内的角色不能引用UserTypeEnum内的值,只能手填hard code;

效果:

与在WebSecurityConfig配置的效果是一样的,但该方式可对每个API进行单独配置,不会导致WebSecurityConfig配置在复杂应用里头的配置很长,并且对开发人员更加直观,授权管理也更加灵活;

总结

至此,我们学会了对API、资源进行授权管理,若结合之前学的访问认证,则我们已能够对应用进行灵活的访问控制和权限控制,可以满足大部分认证授权场景!

如果本文对您有帮助,麻烦点赞+关注!

谢谢!

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