Spring Security(一) 源码分析及认证流程

Spring Security & Oauth2系列:
 
Spring Security(一) 源码分析及认证流程
Spring Security(二)OAuth2认证详解及自定义异常处理

1、Spring Security 概述

Spring Security 是能够为Spring 企业应用提供声明式的安全访问控制解决方案的安全框架,为应用系统提供声明式的安全访问控制功能,减少为企业系统安全访问控制编写大量重复的代码。

1.1 Spring Security项目核心模块

spring-security
├── 核心 - spring-security-core.jar
├── Remoting - spring-security-remoting.jar
├── Web - spring-security-web.jar
├── 配置 - spring-security-config.jar
├── LDAP - spring-security-ldap.jar
├── OAuth 2.0核心 - spring-security-oauth2-core.jar
├── OAuth 2.0客户端 - spring-security-oauth2-client.jar
├── OAuth 2.0 JOSE - spring-security-oauth2-jose.jar
├── ACL - spring-security-acl.jar
├── CAS - spring-security-cas.jar
├── OpenID - spring-security-openid.jar
└── 测试 - spring-security-test.jar

核心详细描述参考中文官网

1.2 Spring Security 简单集成

Spring Security 支持Maven和Gradle集成,本文主要使用Spring Boot与Maven:
pom.xml

<dependencies>
    <!-- ... other dependency elements ... -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

1.2.1 使用Security安全功能

  • 增加配置类,使用注解@EnableWebSecurity开启Web安全功能
  • 实现了接口WebSecurityConfigurer或者继承自WebSecurityConfigurerAdapter),增加Security安全配置。

代码清单:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests((authorize) -> authorize
            .antMatchers("/css/**", "/index").permitAll()
            .antMatchers("/user/**").hasRole("USER")
        )
        .formLogin((formLogin) -> formLogin
            .loginPage("/login")
            .failureUrl("/login-error")
        );
  }
  @Bean
  public UserDetailsService userDetailsService() {
    UserDetails userDetails = User.withDefaultPasswordEncoder()
        .username("user")
        .password("password")
        .roles("USER")
        .build();
    return new InMemoryUserDetailsManager(userDetails);
  }
}

当添加了SecurityConfig 之后我们的应用就具备如下功能:

2、源码分析-Security安全认证实现

2.1 Security安全认证过程类图

类图

2.2 Security安全认证自动化配置

2.2.1 @EnableWebSecurity

从类关系图可以清楚@EnableWebSecurity注解是开启Security安全功能的核心注解,EnableWebSecurity源码清单:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
        HttpSecurityConfiguration.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;

}

@EnableWebSecurity是组合注解,引入了Impost注解包含的外部配置以及激活了@EnableGlobalAuthentication注解,而@EnableGlobalAuthentication注解激活了AuthenticationConfiguration认证配置类。

2.2.2 WebSecurityConfiguration

WebSecurityConfiguration 是web安全配置核心类,WebSecurityConfiguration最主要的功能就是创建了springSecurityFilterChain Bean,springSecurityFilterChain 是spring security的核心过滤器,是整个认证的入口。WebSecurityConfiguration中完成了声明springSecurityFilterChain的作用,并且最终交给DelegatingFilterProxy这个代理类,负责拦截请求(注意DelegatingFilterProxy这个类不是spring security包中的,而是存在于web包中,spring使用了代理模式来实现安全过滤的解耦)。WebSecurityConfiguration源码清单:

@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
//省略==========================================
    @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    public Filter springSecurityFilterChain() throws Exception {
        boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
        boolean hasFilterChain = !this.securityFilterChains.isEmpty();
        Assert.state(!(hasConfigurers && hasFilterChain),
                "Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.");
        if (!hasConfigurers && !hasFilterChain) {
            WebSecurityConfigurerAdapter adapter = this.objectObjectPostProcessor
                    .postProcess(new WebSecurityConfigurerAdapter() {
                    });
            this.webSecurity.apply(adapter);
        }
        for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
            this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
            for (Filter filter : securityFilterChain.getFilters()) {
                if (filter instanceof FilterSecurityInterceptor) {
                    this.webSecurity.securityInterceptor((FilterSecurityInterceptor) filter);
                    break;
                }
            }
        }
        for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
            customizer.customize(this.webSecurity);
        }
        return this.webSecurity.build();
    }

    //省略=====================

}

2.2.3 SpringWebMvcImportSelector

SpringWebMvcImportSelector主要作用是判断当前的环境是否包含springmvc,因为spring security可以在非spring环境下使用,为了避免DispatcherServlet的重复配置,所以使用了这个注解来区分。

2.2.4 OAuth2ImportSelector

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

2.2.5 HttpSecurityConfiguration

HttpSecurityConfiguration配置类首先通过@Autowired去获取容器中的一个AuthenticationManager实例,如果没能获取到则使用依赖注入的AuthenticationConfiguration实例创建一个AuthenticationManager实例,这个实例其实就是ProviderManager。然后初始化httpSecurity。

2.2.5.1 HttpSecurity

通过HttpSecurity配置指明了Web Security 拦截什么URL、登录认证方式、设置什么权限等。

2.2.6 AuthenticationConfiguration

AuthenticationConfiguration 主要作用就是创建全局的身份认证管理者AuthenticationManager,AuthenticationManager便是最核心的身份认证管理器。
AuthenticationConfiguration源码清单:

@Configuration(proxyBeanMethods = false)
@Import(ObjectPostProcessorConfiguration.class)
public class AuthenticationConfiguration {

    //省略================

    @Bean
    public AuthenticationManagerBuilder authenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor,
            ApplicationContext context) {
        LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context);
        AuthenticationEventPublisher authenticationEventPublisher = getBeanOrNull(context,
                AuthenticationEventPublisher.class);
        DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder(
                objectPostProcessor, defaultPasswordEncoder);
        if (authenticationEventPublisher != null) {
            result.authenticationEventPublisher(authenticationEventPublisher);
        }
        return result;
    }

    @Bean
    public static GlobalAuthenticationConfigurerAdapter enableGlobalAuthenticationAutowiredConfigurer(
            ApplicationContext context) {
        return new EnableGlobalAuthenticationAutowiredConfigurer(context);
    }

    @Bean
    public static InitializeUserDetailsBeanManagerConfigurer initializeUserDetailsBeanManagerConfigurer(
            ApplicationContext context) {
        return new InitializeUserDetailsBeanManagerConfigurer(context);
    }

    @Bean
    public static InitializeAuthenticationProviderBeanManagerConfigurer initializeAuthenticationProviderBeanManagerConfigurer(
            ApplicationContext context) {
        return new InitializeAuthenticationProviderBeanManagerConfigurer(context);
    }

    public AuthenticationManager getAuthenticationManager() throws Exception {
        if (this.authenticationManagerInitialized) {
            return this.authenticationManager;
        }
        AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class);
        if (this.buildingAuthenticationManager.getAndSet(true)) {
            return new AuthenticationManagerDelegator(authBuilder);
        }
        for (GlobalAuthenticationConfigurerAdapter config : this.globalAuthConfigurers) {
            authBuilder.apply(config);
        }
        this.authenticationManager = authBuilder.build();
        if (this.authenticationManager == null) {
            this.authenticationManager = getAuthenticationManagerBean();
        }
        this.authenticationManagerInitialized = true;
        return this.authenticationManager;
    }

//省略================

    private AuthenticationManager getAuthenticationManagerBean() {
        return lazyBean(AuthenticationManager.class);
    }

2.3 验证逻辑

2.3.1 AuthenticationManager

AuthenticationManager 提供了认证的入口,源码清单:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager 接收 Authentication 对象作为参数,并通过 authenticate(Authentication) 方法对其进行验证;AuthenticationProvider实现类用来支撑对 Authentication 对象的验证动作;UsernamePasswordAuthenticationToken实现了 Authentication主要是将用户输入的用户名和密码进行封装,并供给 AuthenticationManager 进行验证;验证完成以后将返回一个认证成功的 Authentication 对象;

2.3.2 Authentication

Authentication 源码:

public interface Authentication extends Principal, Serializable {

    //#1.权限集合
    Collection<? extends GrantedAuthority> getAuthorities();
    //#2. 用户密码认证时,可以理解为密码
    Object getCredentials();
     //#3. 认证时包含的信息
    Object getDetails();
    //# 4. 用户密码认证时,可以理解为用户名
    Object getPrincipal();
    //# 5. 是否被认证,认证为true
    boolean isAuthenticated();
    //# 6. 设置是否能够被认证
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

2.3.3 ProviderManager

ProviderManager 它是 AuthenticationManager 的一个实现类,提供了基本的认证逻辑和方法;它包含了一个 List<AuthenticationProvider> 对象,通过 AuthenticationProvider 接口来扩展出不同的认证提供者(当Spring Security默认提供的实现类不能满足需求的时候可以扩展AuthenticationProvider 覆盖supports(Class<?> authentication) 方法);

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                //# 获取当前认证类型
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
                //遍历所有providers,调用supports验证是否支持当前认证
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
                        provider.getClass().getSimpleName(), ++currentPosition, size));
            }
            try {
               //调用provider的认证方法进行认证
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException ex) {
                prepareException(ex, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw ex;
            }
            catch (AuthenticationException ex) {
                lastException = ex;
            }
        }
        if (result == null && this.parent != null) {
            // Allow the parent to try.
            try {
                 //认证失败,调用父类的认证方法进行认证
                parentResult = this.parent.authenticate(authentication);
                result = parentResult;

            }
            catch (ProviderNotFoundException ex) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException ex) {
                parentException = ex;
                lastException = ex;
            }
        }
        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }
            // If the parent AuthenticationManager was attempted and successful then it
            // will publish an AuthenticationSuccessEvent
            // This check prevents a duplicate AuthenticationSuccessEvent if the parent
            // AuthenticationManager already published it
            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).
        if (lastException == null) {
            lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
        }
        // If the parent AuthenticationManager was attempted and failed then it will
        // publish an AbstractAuthenticationFailureEvent
        // This check prevents a duplicate AbstractAuthenticationFailureEvent if the
        // parent AuthenticationManager already published it
        if (parentException == null) {
            prepareException(lastException, authentication);
        }
        throw lastException;
    }

2.3.3 AuthenticationProvider

AuthenticationProvider, ProviderManager通过AuthenticationProvider扩展多种认证方法,AuthenticationProvider 本身也就是一个接口,从类图中我们可以看出它的实现类AbstractUserDetailsAuthenticationProvider 和AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider 。

2.3.4 DaoAuthenticationProvider

DaoAuthenticationProvider 是Spring Security中一个核心的Provider,对所有的数据库提供了基本方法和入口。DaoAuthenticationProvider源码清单:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    /**
     * 实现用户密码加密
     */
    private PasswordEncoder passwordEncoder;

    //省略

    /**
     *
     * 实现 additionalAuthenticationChecks 的验证方法(主要验证密码);
     */
    @Override
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }

//省略

    /**
     * 实现了 AbstractUserDetailsAuthenticationProvider retrieveUser 抽象方法
     * 主要是通过注入UserDetailsService接口对象,并调用其接口方法 loadUserByUsername(String username)获取得到相关的用户信息。
     * UserDetailsService接口非常重要。
     */
    @Override
    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;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    //省略
}

2.3.4 AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider主要实现了AuthenticationProvider的接口方法 authenticate 并提供了相关的验证逻辑;

  1. 抽象获取用户信息方法
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;
  1. 三步验证工作:
    i. preAuthenticationChecks
    ii. additionalAuthenticationChecks(抽象方法,子类实现)
    iii. postAuthenticationChecks
  2. 将验证结果封装成UsernamePasswordAuthenticationToken返回。

源码清单:

public abstract class AbstractUserDetailsAuthenticationProvider
        implements AuthenticationProvider, InitializingBean, MessageSourceAware {
//省略
    //抽象验证方法(主要验证密码)
    protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;

//省略

    /**
     *
     * 实现AuthenticationProvider.authenticate验证方法
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));
        String username = determineUsername(authentication);
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                //1. 获取用户信息
                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException ex) {
                this.logger.debug("Failed to find user '" + username + "'");
                if (!this.hideUserNotFoundExceptions) {
                    throw ex;
                }
                throw new BadCredentialsException(this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }
        try {
            //2. 预检查由DefaultPreAuthenticationChecks类实现(主要判断当前用户是否锁定,过期,冻结User接口)
            this.preAuthenticationChecks.check(user);
            //3. 子类实现
            additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException ex) {
            if (!cacheWasUsed) {
                throw ex;
            }
            // There was a problem, so try again after checking
            // we're using latest data (i.e. not from the cache)
            cacheWasUsed = false;
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            this.preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        }
        //#4.检测用户密码是否过期对应#2 的User接口
        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        //4. 将验证结果封装成UsernamePasswordAuthenticationToken
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    private String determineUsername(Authentication authentication) {
        return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
    }

    //封装验证结果
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
            UserDetails user) {
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
                authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        this.logger.debug("Authenticated user");
        return result;
    }

    //抽象获取用户信息接口
    protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;

    //省略

}

2.3.4 UserDetailsService

UserDetailsService接口作为桥梁,是DaoAuthenticationProvier与特定用户信息来源进行解耦的地方,UserDetailsService由UserDetails和UserDetailsManager所构成;UserDetails和UserDetailsManager各司其责,一个是对基本用户信息进行封装,一个是对基本用户信息进行管理;特别注意,UserDetailsService、UserDetails以及UserDetailsManager都是可被用户自定义的扩展点,我们可以继承这些接口提供自己的读取用户来源和管理用户的方法,比如我们可以自己实现一个 与特定 ORM 框架,比如 Mybatis 或者 Hibernate,相关的UserDetailsService和UserDetailsManager;

2.3.4.1 UserDetailsService

UserDetails 验证用户实体

public interface UserDetails extends Serializable {
 #1.权限集合
 Collection<? extends GrantedAuthority> getAuthorities();
 #2.密码  
 String getPassword();
 #3.用户民
 String getUsername();
 #4.用户是否过期
 boolean isAccountNonExpired();
 #5.是否锁定    
 boolean isAccountNonLocked();
 #6.用户密码是否过期    
 boolean isCredentialsNonExpired();
 #7.账号是否可用(可理解为是否删除)
 boolean isEnabled();
}

2.4 认证流程图

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

推荐阅读更多精彩内容