Spring Security 实战干货:Spring Boot 中的 Spring Security 自动配置初探

1. 前言

我们在前几篇对 Spring Security 的用户信息管理机制,密码机制进行了探讨。我们发现 Spring Security Starter相关的 Servlet 自动配置都在spring-boot-autoconfigure-2.1.9.RELEASE(当前 Spring Boot 版本为2.1.9.RELEASE) ??榈穆肪?code>org.springframework.boot.autoconfigure.security.servlet 之下。其实官方提供的Starter组件的自动配置你都能在spring-boot-autoconfigure-2.1.9.RELEASE下找到。今天我们进一步来解密 Spring SecuritySpring Boot 的配置和使用。

2. Spring Boot 下 Spring Security 的自动配置

我们可以通过 org.springframework.boot.autoconfigure.security.servlet 路径下找到 Spring Security 关于Servlet的自动配置类。我们来大致了解一下。

2.1 SecurityAutoConfiguration

 package org.springframework.boot.autoconfigure.security.servlet;
 
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.security.SecurityDataConfiguration;
 import org.springframework.boot.autoconfigure.security.SecurityProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 import org.springframework.security.authentication.AuthenticationEventPublisher;
 import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
 
 /**
  * {@link EnableAutoConfiguration Auto-configuration} for Spring Security.
  *
  * @author Dave Syer
  * @author Andy Wilkinson
  * @author Madhura Bhave
  * @since 1.0.0
  */
 @Configuration
 @ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
 @EnableConfigurationProperties(SecurityProperties.class)
 @Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
       SecurityDataConfiguration.class })
 public class SecurityAutoConfiguration {
 
   @Bean
   @ConditionalOnMissingBean(AuthenticationEventPublisher.class)
   public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
       return new DefaultAuthenticationEventPublisher(publisher);
   }
 
 }

SecurityAutoConfiguration 顾名思义安全配置类。该类引入(@import)了 SpringBootWebSecurityConfigurationWebSecurityEnablerConfigurationSecurityDataConfiguration 三个配置类。 让这三个??榈睦嗌АJ且桓龈春吓渲?,是 Spring Security 自动配置最重要的一个类之一。 Spring Boot 自动配置经常使用这种方式以达到灵活配置的目的,这也是我们研究 Spring Security 自动配置的一个重要入口 同时 SecurityAutoConfiguration 还将 DefaultAuthenticationEventPublisher 作为默认的 AuthenticationEventPublisher 注入 Spring IoC 容器。如果你熟悉 Spring 中的事件机制你就会知道该类是一个 Spring 事件发布器。该类内置了一个HashMap<String, Constructor<? extends AbstractAuthenticationEvent>>维护了认证异常处理和对应异常事件处理逻辑的映射关系,比如账户过期异常 AccountExpiredException 对应认证过期事件AuthenticationFailureExpiredEvent ,也就是说发生不同认证的异常使用不同处理策略。

2.2 SpringBootWebSecurityConfiguration

 @Configuration
 @ConditionalOnClass(WebSecurityConfigurerAdapter.class)
 @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
 @ConditionalOnWebApplication(type = Type.SERVLET)
 public class SpringBootWebSecurityConfiguration {
 
   @Configuration
   @Order(SecurityProperties.BASIC_AUTH_ORDER)
   static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
 
   }
 
 }

这个类是Spring Security 对 Spring Boot Servlet Web 应用的默认配置。核心在于WebSecurityConfigurerAdapter 适配器。从 @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) 我们就能看出 WebSecurityConfigurerAdapter 是安全配置的核心。 默认情况下 DefaultConfigurerAdapter 将以SecurityProperties.BASIC_AUTH_ORDER-5) 的顺序注入 Spring IoC 容器,这是个空实现。如果我们需要个性化可以通过继承 WebSecurityConfigurerAdapter 来实现。我们会在以后的博文重点介绍该类。

2.3 WebSecurityEnablerConfiguration

 @Configuration
 @ConditionalOnBean(WebSecurityConfigurerAdapter.class)
 @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
 @EnableWebSecurity
 public class WebSecurityEnablerConfiguration {
 
 }

该配置类会在SpringBootWebSecurityConfiguration 注入 Spring IoC 容器后启用 @EnableWebSecurity 注解。也就是说 WebSecurityEnablerConfiguration 目的仅仅就是在某些条件下激活 @EnableWebSecurity 注解。那么这个注解都有什么呢?

3. @EnableWebSecurity 注解

 @Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
 @Target(value = { java.lang.annotation.ElementType.TYPE })
 @Documented
 @Import({ WebSecurityConfiguration.class,
       SpringWebMvcImportSelector.class,
       OAuth2ImportSelector.class })
 @EnableGlobalAuthentication
 @Configuration
 public @interface EnableWebSecurity {
 
   /**
    * Controls debugging support for Spring Security. Default is false.
    * @return if true, enables debug support with Spring Security
    */
   boolean debug() default false;
 } 

@Enable* 这类注解都是带配置导入的注解。通过导入一些配置来启用一些特定功能。 @EnableWebSecurity 导入了 WebSecurityConfiguration 、SpringWebMvcImportSelector 、OAuth2ImportSelector 以及启用了 @EnableGlobalAuthentication注解。

3.1 WebSecurityConfiguration

该配置类WebSecurityConfiguration使用一个WebSecurity对象基于用户指定的或者默认的安全配置,你可以通过继承 WebSecurityConfigurerAdapter 或者实现 WebSecurityConfigurer 来定制 WebSecurity 创建一个FilterChainProxy Bean来对用户请求进行安全过滤。这个FilterChainProxy的名称就是 WebSecurityEnablerConfiguration上的 BeanIds.SPRING_SECURITY_FILTER_CHAIN 也就是 springSecurityFilterChain,它是一个Filter,最终会被作为Servlet过滤器链中的一个Filter应用到Servlet容器中。安全处理的策略主要是过滤器的调用顺序。WebSecurityConfiguration 最终会通过 @EnableWebSecurity 应用到系统。

源码分析:

 package org.springframework.security.config.annotation.web.configuration;
 
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 import javax.servlet.Filter;
 
 import org.springframework.beans.factory.BeanClassLoaderAware;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.DependsOn;
 import org.springframework.context.annotation.ImportAware;
 import org.springframework.core.OrderComparator;
 import org.springframework.core.Ordered;
 import org.springframework.core.annotation.AnnotationAttributes;
 import org.springframework.core.annotation.AnnotationUtils;
 import org.springframework.core.annotation.Order;
 import org.springframework.core.type.AnnotationMetadata;
 import org.springframework.security.access.expression.SecurityExpressionHandler;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.SecurityConfigurer;
 import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
 import org.springframework.security.config.annotation.web.builders.WebSecurity;
 import org.springframework.security.context.DelegatingApplicationListener;
 import org.springframework.security.web.FilterChainProxy;
 import org.springframework.security.web.FilterInvocation;
 import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
 
 
 /**
  * Spring Web Security 的配置类 : 
  *  1. 使用一个 WebSecurity 对象基于安全配置创建一个 FilterChainProxy 对象来对用户请求进行安全过滤。 
  *  2. 也会暴露诸如 安全SpEL表达式处理器 SecurityExpressionHandler 等一些类。
  *   
  * @see EnableWebSecurity
  * @see WebSecurity
  *
  * @author Rob Winch
  * @author Keesun Baik
  * @since 3.2
  */
 @Configuration
 public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
   private WebSecurity webSecurity;
  // 是否启用了调试模式,来自注解 @EnableWebSecurity 的属性 debug,缺省值 false
   private Boolean debugEnabled;
 
   private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;
 
   private ClassLoader beanClassLoader;
 
   @Autowired(required = false)
   private ObjectPostProcessor<Object> objectObjectPostProcessor;
   /**
    *
    * 代理监听器 应该时监听 DefaultAuthenticationEventPublisher 的一些处理策略
    */   
   @Bean
   public static DelegatingApplicationListener delegatingApplicationListener() {
       return new DelegatingApplicationListener();
   }
    /**
     *
     * 安全SpEL表达式处理器 SecurityExpressionHandler 缺省为一个 DefaultWebSecurityExpressionHandler
     */   
   @Bean
   @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
   public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() {
       return webSecurity.getExpressionHandler();
   }
 
   /**
    *  Spring Security 核心过滤器  Spring Security Filter Chain  , Bean ID 为 springSecurityFilterChain
    * @return the {@link Filter} that represents the security filter chain
    * @throws Exception
    */
   @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
   public Filter springSecurityFilterChain() throws Exception {
       boolean hasConfigurers = webSecurityConfigurers != null
               && !webSecurityConfigurers.isEmpty();
       if (!hasConfigurers) {
           WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
                   .postProcess(new WebSecurityConfigurerAdapter() {
                   });
           webSecurity.apply(adapter);
       }
       return webSecurity.build();
   }
 
   /**
    *
    * 用于模板 如JSP Freemarker 的一些页面标签按钮控制支持
    * Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP
    * tag support.
    * @return the {@link WebInvocationPrivilegeEvaluator}
    * @throws Exception
    */
   @Bean
   @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
   public WebInvocationPrivilegeEvaluator privilegeEvaluator() throws Exception {
       return webSecurity.getPrivilegeEvaluator();
   }
 
   /**
    *
    * 用于创建web configuration的SecurityConfigurer实例,
    * 注意该参数通过@Value(...)方式注入,对应的bean autowiredWebSecurityConfigurersIgnoreParents
    * 也在该类中定义
    *
    * @param objectPostProcessor the {@link ObjectPostProcessor} used to create a
    * {@link WebSecurity} instance
    * @param webSecurityConfigurers the
    * {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} instances used to
    * create the web configuration
    * @throws Exception
    */
   @Autowired(required = false)
   public void setFilterChainProxySecurityConfigurer(
           ObjectPostProcessor<Object> objectPostProcessor,
           @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
           throws Exception {
       webSecurity = objectPostProcessor
               .postProcess(new WebSecurity(objectPostProcessor));
       if (debugEnabled != null) {
           webSecurity.debug(debugEnabled);
       }
 
       Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE);
 
       Integer previousOrder = null;
       Object previousConfig = null;
       for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
           Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
           if (previousOrder != null && previousOrder.equals(order)) {
               throw new IllegalStateException(
                       "@Order on WebSecurityConfigurers must be unique. Order of "
                               + order + " was already used on " + previousConfig + ", so it cannot be used on "
                               + config + " too.");
           }
           previousOrder = order;
           previousConfig = config;
       }
       for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
           webSecurity.apply(webSecurityConfigurer);
       }
       this.webSecurityConfigurers = webSecurityConfigurers;
   }
    /**
     * 从当前bean容器中获取所有的WebSecurityConfigurer bean。
     * 这些WebSecurityConfigurer通常是由开发人员实现的配置类,并且继承自WebSecurityConfigurerAdapter
     *  
     */   
   @Bean
   public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents(
           ConfigurableListableBeanFactory beanFactory) {
       return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory);
   }
 
   /**
    * A custom verision of the Spring provided AnnotationAwareOrderComparator that uses
    * {@link AnnotationUtils#findAnnotation(Class, Class)} to look on super class
    * instances for the {@link Order} annotation.
    *
    * @author Rob Winch
    * @since 3.2
    */
   private static class AnnotationAwareOrderComparator extends OrderComparator {
       private static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator();
 
       @Override
       protected int getOrder(Object obj) {
           return lookupOrder(obj);
       }
 
       private static int lookupOrder(Object obj) {
           if (obj instanceof Ordered) {
               return ((Ordered) obj).getOrder();
           }
           if (obj != null) {
               Class<?> clazz = (obj instanceof Class ? (Class<?>) obj : obj.getClass());
               Order order = AnnotationUtils.findAnnotation(clazz, Order.class);
               if (order != null) {
                   return order.value();
               }
           }
           return Ordered.LOWEST_PRECEDENCE;
       }
   }
 
   /*
    * 要是为了获取注解 @EnableWebSecurity 的属性 debugEnabled
    *
    * @see org.springframework.context.annotation.ImportAware#setImportMetadata(org.
    * springframework.core.type.AnnotationMetadata)
    */
   public void setImportMetadata(AnnotationMetadata importMetadata) {
       Map<String, Object> enableWebSecurityAttrMap = importMetadata
               .getAnnotationAttributes(EnableWebSecurity.class.getName());
       AnnotationAttributes enableWebSecurityAttrs = AnnotationAttributes
               .fromMap(enableWebSecurityAttrMap);
       debugEnabled = enableWebSecurityAttrs.getBoolean("debug");
       if (webSecurity != null) {
           webSecurity.debug(debugEnabled);
       }
   }
 
   /*
    * (non-Javadoc)
    *
    * @see
    * org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.
    * lang.ClassLoader)
    */
   public void setBeanClassLoader(ClassLoader classLoader) {
       this.beanClassLoader = classLoader;
   }
 }

3.2 SpringWebMvcImportSelector

该类是为了对 Spring Mvc 进行支持的。一旦发现应用使用 Spring Mvc 的核心前置控制器 DispatcherServlet 就会引入 WebMvcSecurityConfiguration 。主要是为了适配 Spring Mvc 。

3.3 OAuth2ImportSelector

该类是为了对 OAuth2.0 开放授权协议进行支持。ClientRegistration 如果被引用,具体点也就是 spring-security-oauth2 模块被启用(引入依赖jar)时。会启用 OAuth2 客户端配置 OAuth2ClientConfiguration 。

3.4 @EnableGlobalAuthentication

这个类主要引入了 AuthenticationConfiguration 目的主要为了构造 认证管理器 AuthenticationManagerAuthenticationManager 十分重要后面我们会进行专门的分析。

4. SecurityFilterAutoConfiguration

我们在 org.springframework.boot.autoconfigure.security.servlet 路径下还发现了一个配置类 SecurityFilterAutoConfiguration 。该类用于向Servlet容器注册一个名称为securityFilterChainRegistration的bean, 实现类是DelegatingFilterProxyRegistrationBean。该 bean 的目的是注册另外一个 Servlet Filter BeanServlet 容器,实现类为 DelegatingFilterProxy 。DelegatingFilterProxy 其实是一个代理过滤器,它被 Servlet 容器用于处理请求时,会将任务委托给指定给自己另外一个Filter bean。对于 SecurityFilterAutoConfiguration,来讲,这个被代理的Filter bean的名字为 springSecurityFilterChain , 也就是我们上面提到过的 Spring Security Web提供的用于请求安全处理的Filter bean,其实现类是 FilterChainProxy

相关的源码分析:

 package org.springframework.boot.autoconfigure.security.servlet;
 
 import java.util.EnumSet;
 import java.util.stream.Collectors;
 
 import javax.servlet.DispatcherType;
 
 import org.springframework.boot.autoconfigure.AutoConfigureAfter;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
 import org.springframework.boot.autoconfigure.security.SecurityProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
 
 @Configuration
 // 仅在 Servlet 环境下生效
 @ConditionalOnWebApplication(type = Type.SERVLET)
 // 确保安全属性配置信息被加载并以bean形式被注册到容器
 @EnableConfigurationProperties(SecurityProperties.class)
 // 仅在特定类存在于 classpath 上时才生效
 @ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class,
       SessionCreationPolicy.class })
 // 指定该配置类在  SecurityAutoConfiguration 配置类应用之后应用       
 @AutoConfigureAfter(SecurityAutoConfiguration.class)
 public class SecurityFilterAutoConfiguration {
 
     // 要注册到 Servlet 容器的 DelegatingFilterProxy Filter的 
     // 目标代理Filter bean的名称 :springSecurityFilterChain
   private static final String DEFAULT_FILTER_NAME = 
           AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;
 
 
     // 定义一个 bean securityFilterChainRegistration, 
     // 该 bean 的目的是注册另外一个 bean 到 Servlet 容器 : 实现类为 DelegatingFilterProxy 的一个 Servlet Filter
     // 该 DelegatingFilterProxy Filter 其实是一个代理过滤器,它被 Servlet 容器用于匹配特定URL模式的请求,
     // 而它会将任务委托给指定给自己的名字为 springSecurityFilterChain 的 Filter, 也就是 Spring Security Web
     // 提供的用于请求安全处理的一个 Filter bean,其实现类是 FilterChainProxy
     // (可以将 1 个 FilterChainProxy 理解为 1 HttpFirewall + n SecurityFilterChain)
   @Bean
   @ConditionalOnBean(name = DEFAULT_FILTER_NAME)
   public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
           SecurityProperties securityProperties) {
       DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
               DEFAULT_FILTER_NAME);
       registration.setOrder(securityProperties.getFilter().getOrder());
       registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
       return registration;
   }
 
   private EnumSet<DispatcherType> getDispatcherTypes(
           SecurityProperties securityProperties) {
       if (securityProperties.getFilter().getDispatcherTypes() == null) {
           return null;
       }
       return securityProperties.getFilter().getDispatcherTypes().stream()
               .map((type) -> DispatcherType.valueOf(type.name())).collect(Collectors
                       .collectingAndThen(Collectors.toSet(), EnumSet::copyOf));
   }
 
 }
 

5. 总结

本文主要对 Spring Security 在 Spring Boot 中的自动配置一些机制进行了粗略的讲解。为什么没有细讲。因为从学习出发有些东西不是我们必须要深入了解的,但是又要知道一点点相关的知识。我们先宏观上有个大致的了解就行。所以在阅读本文一定不要钻牛角尖。粗略知道配置策略、加载策略和一些关键类的作用即可。在你对 Spring Security 有了进一步学习之后,回头认真来看这些配置类会有更深层的思考。 从另一个方面该文也给你阅读 Spring 源码提供了一些思路,学会这些才是最重要的。

关注公众号:码农小胖哥,获取更多资讯

个人博客:https://felord.cn

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

推荐阅读更多精彩内容