Spring Security Architecture(Spring安全框架的体系结构)

1. Introduction(简介)

本篇是关于Spring安全框架的入门指导,主要讲解Spring 安全框架的体系结构,设计思路和组成模块。虽然本文只涵盖了最为基本的应用安全知识,但这些足以帮助开发者消除在使用Spring 安全框架进行开发时遇到的一些困惑。为了完成这些工作,我们来瞧一瞧如何通过Filters(Servlet规范中一种组件)以及更为常用的方法注解来在Web应用中使用安全组件。
如果你需要在更高的层次上理解一个安全的应用是如何工作的,或者你想知道如何定制化应用的安全组件,或者你仅仅只是想要了解一下应用安全方面的知识,那么,都可以通过阅读本篇指导获取你想要的。但是本篇指导并没有打算去说明或者解决超出基本安全范围的问题或者需求(这些工作由其他的指导来完成),但对于一个关于应用安全的初学者来说,这篇指导是非常有用的。这篇指导有很大的篇幅涉及到了Spring Boot,这是因为Spring Boot默认为应用的安全提供了一些支持,这对于我们理解Spring安全框架是如何适配整个Spring体系结构是有帮助的。所有这些适用于Spring Boot应用的方式或者方法,同样适用于那些使用了Spring框架的其他形式的Web应用程序。

2. Authentication(认证) & Access Control(访问控制)

应用程序的安全问题或多或少可以归纳为两个相互独立的基本问题:认证(Authentication,解决身份识别问题,即识别用户身份是否合法)和授权(Authorization ,解决访问权限问题,即允许用户做什么)。有些人使用"access control(访问控制)"来代替"authorization(授权)",虽然这两种说法会给用户带来一些困惑,但由于"authorization(授权)"这个词在有些地方被过度的解释了,这导致"access control(访问控制)"这种说法更有助于我们理解这种控制用户的访问权限的方式。Spring安全框架的体系结构在设计的时候就将认证(authentication)从授权(authorization)中分离出来,并且设计了一些策略能够对这两者进行扩展。

2.1 Authentication(认证)

Spring安全框架中,为认证(Authentication)设计的主要策略接口是org.springframework.security.authentication.AuthenticationManager ,而这个接口只有一个方法:

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}

AuthenticationManagerauthenticate()方法中,用户可以做三件事情:

  1. 如果可以确认输入的参数authentication(是org.springframework.security.core.Authentication的实例对象)代表一个合法的用户身份,那么返回另一个org.springframework.security.core.Authentication实例对象,这个返回的对象通常会带有一个authenticated=true的标记。
  2. 如果可以确认输入的参数authentication代表一个非法的用户身份,那么将抛出一个org.springframework.security.core.AuthenticationException异常。
  3. 如果无法判断输入的参数authentication是否是一个合法的用户身份,可以返回null值。

org.springframework.security.core.AuthenticationException是一个运行时异常。一般情况下,该异常会被应用程序使用专门的处理器进行处理,而如何处理取决于应用程序的形式和用途。换句话说,一般情况下,应用程序并不指望开发者编写代码去捕获和处理这些异常,而是提供默认的策略,由应用程序自身来处理这个异常,比如,应用程序会提供一个界面同时渲染一个页面来告诉用户认证失败,同时后台的HTTP服务也将会发送401状态码,也会根据应用的上下文环境来决定是否携带WWW-Authenticate头部。
最常用的org.springframework.security.authentication.AuthenticationManager实现类是org.springframework.security.authentication.ProviderManager,该类维护了一个由接口org.springframework.security.authentication.AuthenticationProvider的实现类的实例所组成的列表。而org.springframework.security.authentication.AuthenticationProviderorg.springframework.security.authentication.AuthenticationManager相似,区别在于org.springframework.security.authentication.AuthenticationProvider内部包含另一个方法允许调用者测试是否支持传入的org.springframework.security.core.Authentication实现类的类型:

public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    
    boolean supports(Class<?> authentication);
}

supports()方法中的Class<?>参数的真正类型是Class<? extens Authentication>,主要是用来测试是否支持传入authenticate()方法的authentication参数的类型。由于ProviderManager代理了一个AuthenticationProviders链,所以可以在同一个应用中支持多种不同的认证机制。一般情况下,ProviderManager会跳过那些自己不支持的Authentication的实例类型。
一个ProviderManager有一个可选的父级provider,如果所有的providers都返回的是null,也就是说所有的认证机制都无法确定当前的用户身份是合法的,最终将由这个父级(或者全局)的provider来决定,如果不存在这个父级的provider也会返回null,最终会抛出AuthenticationException
有时候,应用将被保护的资源按照一定的规则分成逻辑上的分组(比如,所有的web资源都通过路径来分组,即将资源按照linux目录树的形式进行分组),并且每一个分组都拥有专属的AuthenticationManager,而这些AuthenticationManager通常都是一个ProviderManager,这些AuthenticationManager共享一个父级(或者全局)的AuthenticationManager,这个父级的AuthenticationManager作为所有的providers的替补而存在。

图一. 由ProviderManager组成的AuthenticationManager层级结构

2.2 Customizing Authentication Managers(自定义认证管理器)

Spring安全框架提供了一些配置的辅助类,能够在应用中快速的创建常用的认证管理功能。最常用的辅助类就是org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder,该类主要用来设置获取用户信息的方式,有三种方式,分别是in-memory,JDBC或者LDAP,也可以添加实现了org.springframework.security.core.userdetails.UserDetailsService接口的类对象来设置获取用户详情的方式。
下面的例子展示了如何在一个应用中配置全局的AuthenticationManager

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

  @Autowired
  public initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

这是一个web应用的例子,AuthenticationManagerBuilder的使用方式有很多种,而在这个例子中AuthenticationManagerBuilder的一个实例被应用作为参数通过@Autowired注解传入initialize()方法中,在这个方法中将会创建一个全局(父级)的AuthenticationManager。我们可以和另外一种使用方式进行对比:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

  @Override
  public configure(AuthenticationManagerBuilder builder) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

上例中使用@Override注解,覆盖了父类的configure()方法,在这个方法中,AuthenticationManagerBuilder的实例只是被该方法的调用者使用来创建一个局部的AuthenticationManager,这个局部的AuthenticationManager是全局AuthenticationManager的孩子。在一个Spring Boot应用中,我们可以使用@Autowired注解将全局的AuthenticationManager注入到其他的bean中,但是无法将一个局部的AuthenticationManager注入到其他的bean中,除非我们通过配置@Bean的方式明确的将该局部的AuthenticationManager的实例作为一个组件发布出去。
Spring Boot提供了一个默认的全局AuthenticationManager,我们可以通过提供自己的AuthenticationManager组件来替换他。这个默认的AuthenticationManager组件是足够安全的,我们无需对他有过多的担心,除非你确实需要一个自定义的AuthenticationManager。如果我们做了一些配置创建了一个AuthenticationManager组件,我们可以将该组件应用到局部的受保护资源上而无需担心全局的AuthenticationManager

前文说的全局的provider,是ProviderManager组件,ProviderManagerAuthenticationManager最常用的一个实现类,所以全局的provider就是全局的AuthenticationManager组件。

2.3 Authorization or Access Control(授权或访问控制)

一旦用户成功通过了认证,我们就可以开始关注授权问题,核心的授权策略由接口org.springframework.security.access.AccessDecisionManager来提供。Spring安全框架提供这个接口的三种实现,每一种都可以委托给一个org.springframework.security.access.AccessDecisionVoter<S>链,这与ProviderManager委托给AuthenticationProviders有一点类似。
一个AccessDecisionVoter主要用来处理代表用户身份的Authentication对象和一个由ConfigAttributes描述的安全对象:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object,
        Collection<ConfigAttribute> attributes);

这个安全对象指的是参数object,其类型就是AccessDecisionVoter<S>的泛型参数S,他代表任何用户想要访问的目标(Web资源或Java类的方法是该对象的两种最常见的形式)。ConfigAttributes指的是参数attributes,他是org.springframework.security.access.ConfigAttribute的集合,保存了安全对象的元数据,而这些元数据确定了访问该安全对象所要求的权限。ConfigAttribute是只有一个方法的接口,而这个方法返回一个String类型的值,这个值通常情况下是一串编码,表示资源所有者制定的资源访问规则。典型的ConfigAttribute应用方式就是返回表示用户角色的字符串,比如(ROLE_ADMIN或者ROLE_AUDIT),这些都有统一的格式(比如以ROLE_为前缀),而另外一种应用方式是返回能够用来执行的表达式字符串。
绝大多开发者只使用默认的AccessDecisionManager组件,默认的AccessDecisionManager的机制是如果得票数没有下降,那么访问就应该被允许。因此所有的定制化开发都倾向于发生在投票者那里,要么是增加新的投票者,要么改变已有投票者的行为。
最常用的ConfigAttributes是Spring的EL表达式,例如isFullyAuthenticated() && hasRole('FOO')。一种AccessDecisionVoter组件就支持Spring的EL表达式,他不但能够执行这些表达式,同时还能为他们创建上下文环境。如果想要扩展表达式的语法,可以实现org.springframework.security.access.expression.SecurityExpressionRoot抽象类或者实现org.springframework.security.access.expression.SecurityExpressionHandler<T>接口。

3. Web Security(Web安全)

3.1 Web Security基本组件

Spring安全框架在Web层的组件都是基于Servelt规范中的Filters,所以事先弄明白Filters所扮演的角色对与我们理解Web安全是有帮助的。下面的图片展示了Http请求处理器的层级结构。

客户端发送请求到应用,容器决定哪些fitlers以及哪一个servlet可以用来处理这次请求。绝大数情况下,一个servlet只能处理一种请求,但是这些filters组成了一个链,他们按照一定的顺序排列,如果某一个filter想要处理这个请求,那么他可以将这个请求拦截下来,并且进行处理。一个filter也能够改变request请求或者response回复。Filter链的顺序是非常重要的,Spring Boot有两种机制在管理filter的顺序 ,一种是在使用@Bean注解发布一个Filter的时候,同时使用@Order注解来指定这个filter的优先级(优先级是由一个int类型的整数表示,数值越大,优先级越高),或者让这个Filter直接实现org.springframework.core.Ordered接口,通过getOrder()方法返回优先级数值,另一种方法是使用org.springframework.boot.web.servlet.FilterRegistrationBean注册Filter的时候,使用他的相关API来指定要注册的Filter的优先级。一些标准的filters通过定义一些常量值来确定他们之间的顺序(比如Spring Session框架中的SessionRepositoryFilter组件,默认的优先级数值由其自身定义的常量DEFAULT_ORDER来表示,其值为Integer.MIN_VALUE + 50,这个值只比int型整数的最小值大一点,因此这个过滤器几乎是排在过滤器链的最下面,要到达这里,必须先通过其他过滤器)。
Spring安全的核心组件就是一个安装到这个过滤器链中的Filter,他的具体类型是org.springframework.security.web.FilterChainProxy,稍后我们将会详细说明这个安全过滤器。在一个Spring Boot应用中,安全过滤器(security filter)是ApplicationContext的一个@Bean,一旦开启了Spring安全功能,就会默认安装这个安全过滤器,并且拦截所有的请求。安全过滤器在过滤器链中的位置由org.springframework.boot.autoconfigure.security.SecurityProperties.DEFAULT_FILTER_ORDER表示的优先级数值来决定,这个值位于锚点org.springframework.boot.web.servlet.FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER的下方(这个值是Spring Boot应用中最大的过滤器优先级数值,因为Spring Boot希望请求在通过整个处理流程之前,先被这个过滤器包装一下,改变一下行为)。从下图我们可以看到,Spring安全框架所提供的功能由单个Filter来提供,但是在这个Filter中,包含着多个内部filters,并且每一个都具有特定的功能。图片如下:

图2. Spring安全的核心组件是一个Filter,他代理了一个内部的过滤器链

在Spring应用中,过滤器通常是安装在类型为org.springframework.web.filter.DelegatingFilterProxy的代理容器中,这种容器并不以Spring的@Bean的形式存在,而是作为原生的Servlet规范中的Filter组件安装到Servlet容器中。Spring安全的过滤器组件就是安装在这种代理容器中,是一个类型为org.springframework.security.web.FilterChainProxy且具有固定名字springSecurityFilterChain的过滤器,这个安全过滤器是以Spring@Bean的形式存在的。而springSecurityFilterChain过滤器又包含了一个封装了安全逻辑的有序过滤器链,组成这个链的过滤器都有相同的API(通常是实现了Servlet规范中Filter接口)并且每一个过滤器都可能将请求拦截到自己这一层并进行处理。所以,事实上的安全层可不止一层。
当然,springSecurityFilterChain也可能会管理多个不同的过滤器链,也就是包含一个过滤器链的列表,并且,所有这些过滤器对容器都是透明的。并且springSecurityFilterChain会将请求派发给第一个匹配的过滤器链。下图展示了基于请求路径的派发过程(这也是使用最多但并不唯一的方式)。这种派发过程的最重要的特点就是有且只有一个过滤器链来处理这个请求。

图3. Spring安全框架的FilterChainProxy将请求派发给第一个匹配请求路径的过滤器链

一个没有任何定制化配置的Spring Boot应用具有6个过滤器链,前5个过滤器链只会忽略那些指向静态资源的路径,例如/css/**/images/**,以及用来展示错误信息视图的路径/error(这些忽略的路径可以在SecurityProperties配置bean中,使用security.ignored来控制)。最后一个过滤器链匹配所有的路径/**并且也是最活跃的,包含认证和授权的逻辑,错误处理,会话处理,头部信息处理等。在这些默认的过滤器链中有总共11个过滤器,通常情况下,用户无需去关心哪个过滤器被使用了以及是什么时候使用的。

注意
Spring安全的所有内部过滤器对于容器来说都是透明的,这很重要,特别是Spring Boot应用,因为所有Filter类型的@Bean都是由容器自动注册的。所以如果你想要添加自定义的安全过滤器到Spring安全的过滤器链中,那么你最好不要通过配置Filter类型的@Bean的方式来添加,因为这样Spring应用会把过滤器注册到容器中而不是添加到Spring安全的过滤器链中,你可以通过将自定义的安全过滤器封装在FilterRegistrationBean中来达成目的。

3.2 Creating and Customizing Filter Chains(创建和定制过滤器链)

在Spring Boot应用程序中,有一个默认的后备过滤器链(该过滤器链匹配所有的请求路径/**)有一个预定义的顺序值SecurityProperties.BASIC_AUTH_ORDER,你可以通过设置security.basic.enabled=false来彻底关闭它,或者你可以只把这个过滤器当作一个定义了一些其他规则且具有一个低优先级的后备。如果想要这样做的话,只需要添加WebSecurityConfigurerAdapter(或者WebSecurityConfigurer)类型的@Bean并且添加@Order注解即可。例如:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
     ...;
  }
}

上例中的@Configurationbean将会使Spring安全框架添加一个优先级排在后备过滤器链前面的新过滤器链。

许多的应用程序拥有访问规则互不相同的资源组。例如:一个应用程序提供的资源包括用户UI和后台API接口两个部分,对于用户UI,支持基于Cookie的认证,而未认证的请求会重定向至登陆页面;而对于后台API接口,则支持基于令牌的认证,未认证的请求会收到携带401状态码的回复。每一个资源组都有他自己的WebSecurityConfigurerAdapter,并且具有唯一的优先级以及请求路径的匹配规则。如果匹配规则发生重叠,那么优先级更高的过滤器链将会胜出。

3.3 Request Matching for Dispatch and Authorization(针对派发和授权的请求匹配)

一个安全过滤器链(等同与一个WebSecurityConfigurerAdapter)持有一个请求匹配器,这个匹配器被用来决定该过滤器链是否适用于当前的HTTP请求。一旦一个HTTP请求适用与一个特定的过滤器链,其他的过滤器链则不会被应用于这个HTTP请求。但是在一个过滤器链内部,你可以使用HttpSecurity来配置额外的匹配器,这样你就可以拥有更细粒度的授权控制。例如:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
      .authorizeRequests()
        .antMatchers("/foo/bar").hasRole("BAR")
        .antMatchers("/foo/spam").hasRole("SPAM")
        .anyRequest().isAuthenticated();
  }
}

在配置Spring安全的时候最容易犯的一个错误就是忘记了这些匹配器将应用于不同的程序,一个是请求匹配器,将应用于整个过滤器链,而其他的匹配器仅仅是用来选择访问规则。

3.4 Combining Application Security Rules with Actuator Rules(应用程序的安全规则与监控规则的整合)

如果你在使用Spring Boot Actuator来监控应用程序的端点(即由path所指向的资源),你应该希望他们是安全的并且默认他们是安全的。实际上,当你将Spring Boot监控功能添加到一个安全的应用程序中时,同时会添加一个过滤器链,而这个过滤器链只会拦截访问Spring Boot监控端点路径的请求。这个过滤器链定义了一个请求匹配器,这个匹配器只匹配监控端点路径,并且具有一个值为ManagementServerProperties.BASIC_AUTH_ORDER的优先级,这个优先级只比默认的SecurityProperties替补过滤器高一点(数值小5),所以匹配监控端点路径的请求会先到达这个过滤器链。
如果你想要你的应用程序的安全规则应用到监控功能端点上,你可以添加一个优先级高于监控端点过滤器链的新的过滤器链,并且让这个新过滤器拦截所有访问监控端点的请求。如果你更倾向于对监控端点使用默认的安全配置,那么最简单的做法就是将你自己的过滤器链添加到监控接口过滤器的后面和替补过滤器的前面。示例如下:

@Configuration
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
     ...;
  }
}

注意
Spring安全框架在Web层与Servlet的API是绑定的,所以现阶段,Spring安全框架只能应用于基于Servlet规范,运行在Servlet容器中的应用程序,而无论容器是否是嵌入式的。当然,Spring安全框架并没有与Spring MVC框架或者Spring的Web技术栈绑定,所以他能够应用于所有基于Servlet规范的应用程序。

4. Method Security(方法安全)

Spring安全框架不仅仅支持Web应用程序,也为Java方法的执行提供安全的访问规则支持。对于Spring安全来说,Java方法只是一种其他形式的“受保护资源”。这就意味着方法的访问规则是与ConfigAttribute形式一样的字符串(比如角色或者表达式),只是应用在你的代码不同的地方。如何引入方法安全功能呢?第一步就是开启方法安全功能,例如:

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

然后我们就可以直接在方法资源上加注解,例如:

@Service
public class MyService {

  @Secured("ROLE_USER")
  public String secure() {
    return "Hello Security";
  }

}

上面的示例是一个含有安全方法的业务类。如果Spring像上面的例子那样创建这种类型的@Bean,那么在这些方法真正被执行之前,这个类会被代理,同时调用者将需要先通过一个安全的拦截器。如果访问被拒绝,那么调用者将会得到一个AccessDeniedException而非该方法的正确执行结果。
当然,还有其他的注解类型可以应用在方法来执行安全限制,比如@PreAuthorize@PostAuthorize,这些注解都允许你编写含有指向方法参数和方法返回值的引用的表达式。

提示
将Web安全和方法安全结合起来使用并非是不常用的。Web安全过滤器链提供用户粒度的安全功能,例如认证和重定向到登陆页面,而方法安全能够提供更细粒度的安全保证。

5. Working with Threads(工作线程)

Spring安全本身就是一个基本的线程边界,因为当前身份被认证后,仍然需要被各种下游消费者所使用。最基本的构造块是org.springframework.security.core.context.SecurityContext的实例对象,他包含了一个Authentication对象(并且如果用户已经登陆,那么这个Authentication是被明确标记为authenticated的)。你可以通过org.springframework.security.core.context.SecurityContextHolder的静态方法方便的访问和使用保存在ThreadLocal中的SecurityContext实例。例如:

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

虽然对于用户应用程序来说,上面的代码并不常用,但这并不代表他没有用处,相反对于开发者来说,这段代码非常有用,比如在开发者想要定制化编写一个认证过滤器的时候(尽管认证过滤器在Spring安全中是最基本的类,开发者编写该类的时候应该尽量避免使用SecurityContextHolder).
如果你想要在一个Web接口中访问当前的已认证用户,你可以在@RequestMapping使用方法参数注解@AuthenticationPrincipal来注入持有用户信息的对象。例如:

@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
  ... // do stuff with user
}

@AuthenticationPrincipal注解会SecurityContext实例中抽取当前Authentication对象并且调用其getPrincipal()方法来获取用户身份对象,然后将用户身份对象注入到方法参数中。Authentication持有的Principal(用户身份)的类型取决于AuthenticationManager验证认证时所使用的类型。
如果Spring安全从HttpServletRequest中获取的Principal就是Authentication类型的,那么开发者可以用这种方式来直接使用:

@RequestMapping("/foo")
public String foo(Principal principal) {
  Authentication authentication = (Authentication) principal;
  User = (User) authentication.getPrincipal();
  ... // do stuff with user
}

5.1 Processing Secure Methods Asynchronously(以异步的方式处理安全的方法)

自从SecurityContext成为线程边界后,如果你想要调用安全的方法做一些异步的后台操作,比如使用@Async注解,那么你需要确保这个上下文是可传播的。说白了就是将SecurityContext封装到task(RunnableCallable等)中,然后交给后面的线程去执行。Spring安全提供了一些辅助类来帮助开发者更方便地完成这个过程。当然,为了传播SecurityContext@Async方法中,开发者需要提供一个AsyncConfigurer同时要确保Executor的正确类型:

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

推荐阅读更多精彩内容