SpringSecurity登录原理(源码级讲解)

一、简单叙述

首先会进入UsernamePasswordAuthenticationFilter并且设置权限为null和是否授权为false,然后进入ProviderManager查找支持UsernamepasswordAuthenticationTokenprovider并且调用provider.authenticate(authentication);再然后就是UserDetailsService接口的实现类(也就是自己真正具体的业务了),这时候都检查过了后,就会回调UsernamePasswordAuthenticationFilter并且设置权限(具体业务所查出的权限)和设置授权为true(因为这时候确实所有关卡都检查过了)。

PS:云里雾绕的?没关系,接下里看我们每一步骤都具体的深入到源码级别的去分析。


二、源码分析

UsernamePasswordAuthenticationFilter

// 继承了AbstractAuthenticationProcessingFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        // 认证请求的方式必须为POST
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        // 获取用户名
        String username = obtainUsername(request);
        // 获取密码
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }
        // 用户名去空白
        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

可以发现继承了AbstractAuthenticationProcessingFilter,那我们就来看下此类

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
    
    // 过滤器doFilter方法
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        /*
         * 判断当前filter是否可以处理当前请求,若不行,则交给下一个filter去处理。
         */
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
            // 很关键!!!调用了子类(UsernamePasswordAuthenticationFilter)的方法
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            // 最终认证成功后,会处理一些与session相关的方法(比如将认证信息存到session等操作)。
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            // 认证失败后的一些处理。
            unsuccessfulAuthentication(request, response, failed);

            return;
        }
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        /*
         * 最终认证成功后的相关回调方法,主要将当前的认证信息放到SecurityContextHolder中
         * 并调用成功处理器做相应的操作。
         */
        successfulAuthentication(request, response, chain, authResult);
    }
}

PS:看到这里估计很多人在骂娘了,什么玩意,直接复制粘贴也不讲解,不要急,上面只是看下类结构,下面来具体分析!这里只分析主要代码,不是很主要也不是很相关的不作讲解,有兴趣的自己去读。

(一)、 父类的处理流程

1、继承了父类,父类是个过滤器,所以肯定先执行AbstractAuthenticationProcessingFilter.doFilter(),此方法首先判断当前的filter是否可以处理当前请求,不可以的话则交给下一个filter处理。

/*
* 判断当前filter是否可以处理当前请求,若不行,则交给下一个filter去处理。
*/
if (!requiresAuthentication(request, response)) {
    chain.doFilter(request, response);
    return;
}

2、调用此抽象类的子类UsernamePasswordAuthenticationFilter.attemptAuthentication(request, response)方法做具体的操作。

// 很关键!!!调用了子类(UsernamePasswordAuthenticationFilter)的方法
authResult = attemptAuthentication(request, response);

3、最终认证成功后做一些成功后的session操作,比如将认证信息存到session等。

// 最终认证成功后,会处理一些与session相关的方法(比如将认证信息存到session等操作)。
sessionStrategy.onAuthentication(authResult, request, response);

4、最终认证成功后的相关回调方法,主要将当前的认证信息放到SecurityContextHolder中并调用成功处理器做相应的操作。

successfulAuthentication(request, response, chain, authResult);

protected void successfulAuthentication(HttpServletRequest request,
    HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
    }
    
    // 将当前的认证信息放到SecurityContextHolder中
    SecurityContextHolder.getContext().setAuthentication(authResult);
    rememberMeServices.loginSuccess(request, response, authResult);
    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                authResult, this.getClass()));
    }
    // 调用成功处理器,可以自己实现AuthenticationSuccessHandler接口重写方法写自己的逻辑
    successHandler.onAuthenticationSuccess(request, response, authResult);
}

(二)、子类的处理流程

1、父类的authResult = attemptAuthentication(request, response);触发了自类的方法。

2、此方法首先判断请求方式是不是POST提交,必须是POST

// 认证请求的方式必须为POST
if (postOnly && !request.getMethod().equals("POST")) {
    throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
}

3、从请求中获取usernamepassword,并做一些处理

// 获取用户名
String username = obtainUsername(request);
// 获取密码
String password = obtainPassword(request);

if (username == null) {
    username = "";
}

if (password == null) {
    password = "";
}
// 用户名去空白
username = username.trim();

4、封装Authenticaiton类的实现类UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    super((Collection)null);
    this.principal = principal;
    this.credentials = credentials;
    this.setAuthenticated(false);
}

PS:为什么这个构造器设置权限为null?super((Collection)null);,并且设置是否授权为false?this.setAuthenticated(false);

道理很简单,因为我们这是刚刚登陆过来,你的账号密码对不对我们都没验证呢,所以这里是未授权,权限null。

5、调用AuthenticationManagerauthenticate方法进行验证

return this.getAuthenticationManager().authenticate(authRequest);

(三)、AuthenticationManager处理流程

1、怎么触发的?

return this.getAuthenticationManager().authenticate(authRequest);

PS:交由AuthenticationManager接口的ProviderManager实现类处理。

2、ProviderManager.authenticate(Authentication authentication);

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class toTest = authentication.getClass();
        Object lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        // 拿到全部的provider
        Iterator e = this.getProviders().iterator();
        // 遍历provider
        while(e.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)e.next();
            // 挨着个的校验是否支持当前token
            if(provider.supports(toTest)) {
                if(debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    // 找到后直接break,并由当前provider来进行校验工作
                    result = provider.authenticate(authentication);
                    if(result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var11) {
                    this.prepareException(var11, authentication);
                    throw var11;
                } catch (InternalAuthenticationServiceException var12) {
                    this.prepareException(var12, authentication);
                    throw var12;
                } catch (AuthenticationException var13) {
                    lastException = var13;
                }
            }
        }
        // 若没有一个支持,则尝试交给父类来执行
        if(result == null && this.parent != null) {
            try {
                result = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var9) {
                ;
            } catch (AuthenticationException var10) {
                lastException = var10;
            }
        }
    ..........................
    }

**3、此方法遍历所有的Providers,然后依次执行验证方法看是否支持UsernamepasswordAuthenticationToken**

// 拿到全部的provider
Iterator e = this.getProviders().iterator();
// 遍历provider
while(e.hasNext()) {
    AuthenticationProvider provider = (AuthenticationProvider)e.next();
    // 挨着个的校验是否支持当前token
    if(provider.supports(toTest)) {
        if(debug) {
            logger.debug("Authentication attempt using " + provider.getClass().getName());
        }
    }
}

4、若有一个能够支持当前token,则直接交由此provider处理并break。

// 找到后直接break,并由当前provider来进行校验工作
result = provider.authenticate(authentication);
if(result != null) {
    this.copyDetails(authentication, result);
    break;
}

5、若没一个provider验证成功,则交由父类来尝试处理

// 若没有一个支持,则尝试交给父类来执行
if(result == null && this.parent != null) {
    try {
        result = this.parent.authenticate(authentication);
    } catch (ProviderNotFoundException var9) {
        ;
    } catch (AuthenticationException var10) {
        lastException = var10;
    }
}

(四)、AuthenticationProvider处理流程

1、怎么触发的?

// 由上一步的ProviderManager的authenticate方法来触发
result = provider.authenticate(authentication);

PS:这里交由AuthenticationProvider接口的实现类DaoAuthenticationProvider来处理。

2、DaoAuthenticationProvider

// 继承了AbstractUserDetailsAuthenticationProvider
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        UserDetails loadedUser;
        try {
            /*
             * 调用UserDetailsService接口的loadUserByUsername方法,
             * 此方法就是我们自己定义的类去实现接口重写的方法,处理我们自己的业务逻辑。
             */
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        } catch (UsernameNotFoundException var6) {
            if(authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
            }

            throw var6;
        } catch (Exception var7) {
            throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
        }

        if(loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    }
}

3、继承了AbstractUserDetailsAuthenticationProvider

// 实现了AuthenticationProvider接口
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
  
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
        String username = authentication.getPrincipal() == null?"NONE_PROVIDED":authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if(user == null) {
            cacheWasUsed = false;

            try {
                // 调用自类retrieveUser
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User \'" + username + "\' not found");
                if(this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            /*
             * 前检查由DefaultPreAuthenticationChecks类实现(主要判断当前用户是否锁定,过期,冻结
             * User接口)
             */
            this.preAuthenticationChecks.check(user);
            // 子类具体实现
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if(!cacheWasUsed) {
                throw var7;
            }
            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }
        // 检测用户密码是否过期
        this.postAuthenticationChecks.check(user);
        if(!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if(this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }
}

4、AbstractUserDetailsAuthenticationProvider.authenticate()首先调用了user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);

PS:调用的是DaoAuthenticationProvider.retrieveUser()

5、调用我们自己的业务处理类

 /*
 * 调用UserDetailsService接口的loadUserByUsername方法,
 * 此方法就是我们自己定义的类去实现接口重写的方法,处理我们自己的业务逻辑。
 */
 loadedUser = this.getUserDetailsService().loadUserByUsername(username);

比如:

/**
 * @author chentongwei@bshf360.com 2018-03-26 13:15
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("表单登录用户名:" + username);
        return buildUser(username);
    }

    private UserDetails buildUser(String username) {
        /**
         * passwordEncoder.encode这步骤应该放到注册接口去做,而这里只需要传一个从db查出来的pwd即可。
         *
         * passwordEncoder.encode("123456")每次打印出来都是不同的,虽然是同一个(123456)密码,
         * 但是他会随机生成一个盐(salt),他会把随机生成的盐混到加密的密码里。Springsecurity验证(matches方法)的时候会将利用此盐解析出pwd,进行匹配。
         * 这样的好处是:如果数据库里面有10个123456密码。但是被破解了1个,那么另外九个是安全的,因为db里存的串是不一样的。
         */
        String password = passwordEncoder.encode("123456");
        logger.info("数据库密码是:" + password);
        // 这个User不一定必须用SpringSecurity的,可以写一个自定义实现UserDetails接口的类,然后把是否锁定等判断逻辑写进去。
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

PS:注意:实现UserDetailsService接口。可返回我们自己定义的User类,但User类要实现UserDetails接口

6、调用完retrieveUser方法继续回到抽象类的authenticate方法

7、首先做一些检查

/*
* 前检查由DefaultPreAuthenticationChecks类实现(主要判断当前用户是否锁定,过期,冻结
* User接口)
*/
this.preAuthenticationChecks.check(user);
// 检测用户密码是否过期
this.postAuthenticationChecks.check(user);

8、调用createSuccessAuthentication方法进行授权成功

return this.createSuccessAuthentication(principalToReturn, authentication, user);
// 成功授权
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
    // 回调UsernamePasswordAuthenticationToken的构造器,这里调用的是授权成功的构造器
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
    // 将认证信息的一块内容放到details
    result.setDetails(authentication.getDetails());
    return result;
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
    // 不在是null,而是传来的权限,这个权限就是我们自己定义的detailsService类所返回的,可以从db查
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    // 这里是true,不在是false。
    super.setAuthenticated(true);
}

9、回到起点

AbstractAuthenticationProcessingFilter.doFilter()

进行session存储和成功后的处理器的调用等


三、总结

只是简单说下类之间的调用顺序。

UsernamePasswordAuthenticationFilter
Authentication
AuthenticationManager
AuthenticationProvider
UserDetailsService
// 回到起点进行后续操作,比如缓存认证信息到session和调用成功后的处理器等等
UsernamePasswordAuthenticationFilter 

四、Demo

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="login" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登录</button></td>
        </tr>
    </table>
</form>
</body>
</html>
http.formLogin()
    // 默认表单登录页
    .loginPage(SecurityConstant.DEFAULT_UNAUTHENTICATION_URL)
    // 登录接口
    .loginProcessingUrl(SecurityConstant.DEFAULT_LOGIN_PROCESSING_URL_FORM)
/**
 * 常量
 *
 * @author chentongwei@bshf360.com 2018-03-26 11:40
 */
public interface SecurityConstant {

    /**
     * 默认登录页
     */
    String DEFAULT_LOGIN_PAGE_URL = "/default-login.html";

    /**
     * 默认的登录接口
     */
    String DEFAULT_LOGIN_PROCESSING_URL_FORM = "/login";
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
 * @author chentongwei@bshf360.com 2018-03-26 13:15
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("表单登录用户名:" + username);
        return buildUser(username);
    }

    private UserDetails buildUser(String username) {
        /**
         * passwordEncoder.encode这步骤应该放到注册接口去做,而这里只需要传一个从db查出来的pwd即可。
         *
         * passwordEncoder.encode("123456")每次打印出来都是不同的,虽然是同一个(123456)密码,
         * 但是他会随机生成一个盐(salt),他会把随机生成的盐混到加密的密码里。Springsecurity验证(matches方法)的时候会将利用此盐解析出pwd,进行匹配。
         * 这样的好处是:如果数据库里面有10个123456密码。但是被破解了1个,那么另外九个是安全的,因为db里存的串是不一样的。
         */
        String password = passwordEncoder.encode("123456");
        logger.info("数据库密码是:" + password);
        // 这个User不一定必须用SpringSecurity的,可以写一个自定义实现UserDetails接口的类,然后把是否锁定等判断逻辑写进去。
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

大功告成!

只需要一个html,一段配置,一个Service自己的业务类即可。

疑问:

1、接口login在哪定义的?

2、用户名username和密码password在哪接收的?

3、没有控制器怎么进入我们的MyUserDetailsService的方法?

解答:

1、SpringSecurity内置的,并且只能为POST

public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login", "POST"));
}

2、名称不能变,必须是usernamepassword

public class UsernamePasswordAuthenticationFilter extends
      AbstractAuthenticationProcessingFilter {
   // ~ Static fields/initializers
   // =====================================================================================

   public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
   public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
}

3、自己看我上面的源码分析


五、广告

  • Demo源码已上传到码云,文章会定期更新。下面链接是我对Spring-Security进行的二次封装。使之变得零配置,高扩展。如果觉得对您有帮助,希望给个star,没帮助也可以看看框架思想。

    https://gitee.com/geekerdream/common-security

  • QQ群【Java初学者学习交流群】:458430385

  • 微信公众号【Java码农社区】

    img
  • 今日头条号:编程界的小学生

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

推荐阅读更多精彩内容