spring-security

1. 带着疑问去学习

在使用spring-security时,我们应该去思考一些问题:

  • 系统在启动spring-security时做了哪些事
  • 默认的认证界面如何出现的
  • 默认的认证流程如何实现

2. 流程分析

客户端发起请求到最终的servlet可能会经过如下过程:


请求到servlet

我们可以思考一下,权限认证是不是也是其中某一个或者某几个filter呢?

基于XML分析

<filter>
  <filter-name>springSecurityFilterChain</filter-name> <!-- 这里的名称是固定的 -->
  <filter-class>org.springframeword.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
  <filter-name>springSecurityFilterChain</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

DelegatingFilterProxy并不是SpringSecurity提供,而是Spring框架本身就存在的。它继承自GenericFilterBean,Servlet容器在启动的时候就会执行GenericFilterBean的init方法。

public final void init(FilterConfig filterConfig) throws ServletException {
        Assert.notNull(filterConfig, "FilterConfig must not be null");

        this.filterConfig = filterConfig;

        // Set bean properties from init parameters.
        PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
                Environment env = this.environment;
                if (env == null) {
                    env = new StandardServletEnvironment();
                }
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, env));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            }
            catch (BeansException ex) {
                String msg = "Failed to set bean properties on filter '" +
                    filterConfig.getFilterName() + "': " + ex.getMessage();
                logger.error(msg, ex);
                throw new NestedServletException(msg, ex);
            }
        }

        // 子类扩展,初始化自己的filter.
        initFilterBean();

        if (logger.isDebugEnabled()) {
            logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use");
        }
    }

我们看看子类DelegatingFilterProxy的initFilterBean()方法里面做了什么事

protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                // If no target bean name specified, use filter name.
                if (this.targetBeanName == null) {
                    //这里获取的就是web.xml中配置的filterName=springSecurityFilterChain
                    this.targetBeanName = getFilterName();
                }
                // 获取IOC容器对象.
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    //获取委托处理请求的过滤器,这里的实际过滤是通过FilterChainProxy处理的
                    this.delegate = initDelegate(wac);
                }
            }
        }
    }

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        // 获取的值是springSecurityFilterChain
        String targetBeanName = getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        // SpringIOC容器中是根据springSecurityFilterChain这个名称获取bean对象的,
        // 所以web.xml中必须使用springSecurityFilterChain命名filter, 否则获取不到
        Filter delegate = wac.getBean(targetBeanName, Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }

在debug模式下我们可以看到真实的具体信息


FilterChainProxy

通过上面源码的解析我们能够发现DelegatingFilterProxy这个过滤器在初始的时候从Spring容器中获取了 FilterChainProxy 这个过滤器链的代理对象,并且把这个对象保存在了DelegatingFilterProxy 的delegate属性中。那么当请求到来的时候会执行DelegatingFilterProxy的doFilter方法,那么我们就可以来看下这个方法里面又执行了什么

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        //if中的逻辑在初始化阶段已经完成
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: " +
                                "no ContextLoaderListener or DispatcherServlet registered?");
                    }
                    delegateToUse = initDelegate(wac);
                }
                this.delegate = delegateToUse;
            }
        }

        // 核心代码,调用委托对象处理
        invokeDelegate(delegateToUse, request, response, filterChain);
    }


protected void invokeDelegate(
            Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //这里的delegate属性在之前的初始化阶段被赋值成FilterChainProxy
        delegate.doFilter(request, response, filterChain);
    }

实际流程变为:


spring-security过滤流程
FilterChainProxy请求处理分析
private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        // 对request对象做防火墙检查,校验提交方式是否合法【post, get等】
        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);
        // 核心方法,这里是获取这个请求过滤链中的所有过滤器
        List<Filter> filters = getFilters(fwRequest);
        if (filters == null || filters.size() == 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                        + (filters == null ? " has no matching filters"
                                : " has an empty filter list"));
            }

            fwRequest.reset();

            chain.doFilter(fwRequest, fwResponse);

            return;
        }
       //存在过滤器的情况下,构建一个虚拟的过滤器链路并执行
        VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    }

我们需要了解的概念,SpringSecurity中可以存在多个过滤器链,而每个过滤器链又可以包含多个过滤器


多个过滤器链

多个过滤器链
spring-security中15个核心过滤器
15个核心过滤器

SpringSecurity中的主要过滤器

  • ChannelProcessingFilter
    处理https,没配置就没有?

  • WebAsyncManagerIntegrationFilter
    将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成

  • SecurityContextPersistenceFilter
    new HttpRequestResponseHolder(request,response)
    HttpSessionSecurityContextRepository#loadContext
    从request中获取session,从Session中取出已认证用户的信息保存在SecurityContext中,提高效率, 避免
    每一次请求都要解析用户认证信息,方便接下来的filter直接获取当前的用户信息
    如果是第一次请求,session没有相关信息,那么会创建一个新的SecurityContext
    包装request、response
    SecurityContextHolder.setContext(contextBeforeChainExecution);
    finally
    将上下文保存到HttpSessionSecurityContextRepository
    清除Holder中的上下文

  • HeaderWriterFilter
    往该请求的Header中添加相应的信息,在http标签内部使用security:headers来控制

  • CorsFilter
    未配置就没有

  • CsrfFilter
    对需要验证的请求验证是否包含csrf的token信息,如果不包含,则报错。 这样攻击网站无法获取到
    token信息,则跨域提交的信息都无法通过过滤器的校验

  • LogoutFilter
    根据request的请求方法和路径匹配判断当前是否为注销URL
    默认匹配 POST /logout
    this.handler.logout(request, response, auth);
    CsrfLogoutHandler
    SecurityContextLogoutHandler
    使session失效
    清除remember me
    清除SecurityContextHolder的SecurityContext
    LogoutSuccessEventPublishingLogoutHandler
    通知注销事件
    SimpleUrlLogoutSuccessHandler#onLogoutSuccess(request, response, auth);
    重定向,默认为/login?logout
    ...

  • UsernamePasswordAuthenticationFilter
    遍历本Manager和父Manager的所有AuthenticationProvider 如果有AuthenticationProvider支持处理当前类型的Authentication

    if (!provider.supports(toTest)) {
      continue;
    }
    try {     
    result = provider.authenticate(authentication);
        if (result != null) {
            copyDetails(authentication, result);
            break;
        }
    }
    /** 穿插一点使用的知识点
    1. 可自定义Authenticaton类,实现Authentication接口完成
    2. 自定义provider进行用户身份鉴权,支持类型是自己自定义的Authenticaton即可
    /
    
    try {     
        user = retrieveUser(username,authentication);
    }catch (UsernameNotFoundException notFound){
        throw new BadCredentialsException(xx)
    }
    protected final UserDetails retrieveUser(String username,
              UsernamePasswordAuthenticationToken authentication)
              throws AuthenticationException {
          prepareTimingAttackProtection();
          try {
                          //核心处理,这里可以自定义实现
              UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
              if (loadedUser == null) {
                  throw new InternalAuthenticationServiceException(
                          "UserDetailsService returned null, which is an interface contract violation");
              }
              return loadedUser;
          }
    ...
      }
    // 上述自定义实现,可以通过实现UserDetailService接口,实现loadUserByUsername方法,加载自己业务的用户信息,从数据库中读取啥的
    

    用户相关前置判断
    preAuthenticationChecks.check(user);
    AbstractUserDetailAuthenticationProvider.DefaultPreAuthenticationChecks
    !user.isAccountNonLocked() 账号是否被锁定
    !user.isEnabled() 账号是否可用
    !user.isAccountNonExpired() 账号是否过期
    验证密码是否正确
    additionalAuthenticationChecks(user,authentication)
    如果验证成功
    擦除所有地方的密码信息
    发布AuthenticationSuccessEvent事件
    如果验证失败
    抛出AuthenticationException
    清除SecurityContext
    ...

  • DefaultLoginPageGeneratingFilter

    if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
              //这里是根据身份验证结果,硬编码生成html页面返回
              String loginPageHtml = generateLoginPageHtml(request, loginError,
                      logoutSuccess);
              response.setContentType("text/html;charset=UTF-8");
              response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
              response.getWriter().write(loginPageHtml);
    
              return;
          }
    
  • DefaultLogoutPageGeneratingFilter

    if (this.matcher.matches(request)) {//匹配url  "/logout"
            renderLogout(request, response);//硬编码返回html页面
        } else {
            filterChain.doFilter(request, response);
        }
    
  • ConcurrentSessionFilter
    取出session
    若过期,进入注销逻辑,return
    没过期,更新session
    session就跳过
    ...

  • BasicAuthenticationFilter
    没配置就没有
    处理HTTP请求中的BASIC authorization头部,把认证结果写入SecurityContextHolder

  • RequestCacheAwareFilter
    从缓存中寻找是否已经有解析过的请求,若有,替换掉原生请求
    否则,继续
    SecurityContextHolderAwareRequestFilter
    封装Request,丰富API
    ...

  • RememberMeAuthenticationFilter
    当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统
    AnonymousAuthenticationFilter
    检测 SecurityContextHolder 中的SecurityContext是否存在 Authentication
    如果不存在为其提供一个匿名 Authentication key随机生成,username为anonymous,权限只有ROLE_Anonymous
    authenticated为true
    否则跳过
    ...

  • SessionManagementFilter
    防止会话固定?;すセ?br> 限制已认证用户可以同时打开多少个会话

  • ExceptionTranslationFilter
    直接chain.doFilter(request, response)
    通过catch处理下一个Filter或应用逻辑产生的异常
    AuthenticationException
    AccessDeniedException
    当前是匿名认证或者认证信息不全
    sendStartAuthentication(request,response,chain,new InsufficientAuthenticationException(xx))
    其他
    accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);

  • FilterSecurityInterceptor
    主要用于鉴权逻辑的处理,之前的UsernamePasswordAuthenticationFilter等都是用于登录逻辑处理。

过滤器如何执行

先前我们讲到,会构造出一个虚拟链路执行对应的filter。VirtualFilterChain

public void doFilter(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
            if (currentPosition == size) {
                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " reached end of additional filter chain; proceeding with original chain");
                }

                // Deactivate path stripping as we exit the security filter chain
                this.firewalledRequest.reset();

                originalChain.doFilter(request, response);
            }
            else {
                // size=15  currentPosition初始化为0  会从0-14一个一个取出过滤器执行
                currentPosition++;
                Filter nextFilter = additionalFilters.get(currentPosition - 1);

                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " at position " + currentPosition + " of " + size
                            + " in additional filter chain; firing Filter: '"
                            + nextFilter.getClass().getSimpleName() + "'");
                }
                // 调用取出的过滤器执行对应的过滤方法,此处用到责任链模式
                nextFilter.doFilter(request, response, this);
            }
        }
详细分析ExceptionTranslationFilter、FilterSecurityInterceptor
  1. 在整个过滤器链中,ExceptionTranslationFilter是倒数第二个执行的过滤器,它的作用是通过catch处理下一个Filter【也就是FilterSecurityInterceptor】或应用逻辑产生的异常
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            chain.doFilter(request, response);
        }catch (Exception ex) {
//....省略相关代码
            if (ase != null) {
                                //...省略相关非核心代码
                  //异常核心处理方法
                handleSpringSecurityException(request, response, chain, ase);
            }
        }
    }

private void handleSpringSecurityException(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, RuntimeException exception)
            throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
           //开始登录认证异常处理
            sendStartAuthentication(request, response, chain,
                    (AuthenticationException) exception);//这里实际上是调用了AuthenticationEntryPoint.commence()方法来处理认证异常
        }
        else if (exception instanceof AccessDeniedException) {
                 //开始鉴权异常处理
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
                sendStartAuthentication(
                        request,
                        response,
                        chain,
                        new InsufficientAuthenticationException(
                            messages.getMessage(
                                "ExceptionTranslationFilter.insufficientAuthentication",
                                "Full authentication is required to access this resource")));
            }
            else {
                logger.debug(
                        "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
                        exception);

                accessDeniedHandler.handle(request, response,
                        (AccessDeniedException) exception);
            }
        }
    }
public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {

        String redirectUrl = null;

        if (useForward) {

            if (forceHttps && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page will be
                // used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }

            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request, response,
                        authException);

                if (logger.isDebugEnabled()) {
                    logger.debug("Server side forward to: " + loginForm);
                }

                RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

                dispatcher.forward(request, response);

                return;
            }
        }
        else {
            // 获取重定向的地址, 默认是http://localhost:8080/login

            redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

        }
                //重定向到指定路径页面
        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }
/**
使用说明:
1. 这里的AuthenticationEntryPoint可以自定义逻辑处理,实现对应的commence方法即可
2. 可根据相关逻辑重定向或者直接返回认证失败的信息,依据自身业务来
/
  1. FilterSecurityInterceptor是SpringSecurity过滤器链中的最后一个过滤器,作用是先判断是否身份
    验证,然后在做权限的验证。第一次访问的时候处理的在doFilter中的方法的关键代码如下:
try {
  this.accessDecisionManager.decide(authenticated, object, attributes); 
}catch (AccessDeniedException accessDeniedException){
   //发布AuthorizationFailureEvent事件 
  throw accessDeniedException; }

decide方法在做投票选举,第一次的时候回抛出AccessDeniedException异常,而抛出的异常会
被ExceptionTranslationFilter中的catch语句块捕获,进而执行handleSpringSecurityException方法。

基于SpringBoot方式分析

<dependency> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-security</artifactId> 
</dependency>

基于SpringBoot的自动装配,第三方框架要加载的信息在spring.factories中


自动装配security
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\

这三个类中与DelegatingFilterProxy有关系的是SecurityFilterAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {

    private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;

    @Bean
    @ConditionalOnBean(name = DEFAULT_FILTER_NAME)//name = "springSecurityFilterChain"
    public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
            SecurityProperties securityProperties) {
                //将filter添加到spring容器中
        DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
                DEFAULT_FILTER_NAME);
        registration.setOrder(securityProperties.getFilter().getOrder());
        registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
        return registration;
    }
}

我们来看一下这里面的构建逻辑是如何处理的


类图结构

从ServletContextInitializer.onStartU()开始分析

  • RegistrationBean的onStartUp方法
  • DynamicRegistrationBean的register方法
    protected final void register(String description, ServletContext servletContext) {
          //1. 添加过滤器,DelegatingFilterProxy
          D registration = addRegistration(description, servletContext);
          if (registration == null) {
              logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
              return;
          }
          //2. 设置拦截url  默认是 /* 拦截所有
          configure(registration);
      }
    
    protected Dynamic addRegistration(String description, ServletContext servletContext) {
          Filter filter = getFilter();
          return servletContext.addFilter(getOrDeduceName(filter), filter);
      }
    //这里就很清晰了,显示的声明了一个DelegatingFilterProxy,并且指明beanName=springSecurityFilterChain
    public DelegatingFilterProxy getFilter() {
          return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {
    
              @Override
              protected void initFilterBean() throws ServletException {
                  // Don't initialize filter bean on init()
              }
    
          };
      }
    
    AbstractFilterRegistrationBean
    protected void configure(FilterRegistration.Dynamic registration) {
          .....
          Set<String> servletNames = new LinkedHashSet<>();
          for (ServletRegistrationBean<?> servletRegistrationBean : this.servletRegistrationBeans) {
              servletNames.add(servletRegistrationBean.getServletName());
          }
          servletNames.addAll(this.servletNames);
          // DelegatingFilterProxyRegistrationBean 创建时没有指定ServletRegistrationBeans, urlPatterns。为空
          if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) {
              // DEFAULT_URL_MAPPINGS="/*", 默认拦截所有
              registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, DEFAULT_URL_MAPPINGS);
          }
          else {
              if (!servletNames.isEmpty()) {
                  registration.addMappingForServletNames(dispatcherTypes, this.matchAfter,
                          StringUtils.toStringArray(servletNames));
              }
              if (!this.urlPatterns.isEmpty()) {
                  registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter,
                          StringUtils.toStringArray(this.urlPatterns));
              }
          }
      }
    

至此,我们可以看到在springboot中,通过DelegatingFilterProxyRegistrationBean创建了一个,DelegatingFilterProxy过滤器并且执行了拦截地址是 /*。后续的流程就和xml流程一致,通过FilterChainProxy处理。

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