springsecurity 登录认证原理

一、概述

简介

Spring Security:是一个提供身份验证,授权和保护以防止常见攻击的框架。 凭借对命令式和反应式应用程序的一流支持,它为Spring应用程序的安全提供实际标准。

特性

Spring Security为身份验证,授权和针对常见漏洞的防护提供了全面的支持。 它还集成了三方库,以简化其使用。

认证

Spring Security为身份验证提供了全面的支持。 身份验证是我们验证谁试图访问特定资源的身份的方法。 验证用户身份的常用方法是要求用户输入用户名和密码。 一旦执行了身份验证,我们就会知道身份并可以执行授权。

认证支持

Spring Security提供了用于验证用户的内置支持。 分为两大主体:基于Servlet和WebFlux的身份验证。

密码加密存储

Spring Security的PasswordEncoder接口用于对密码进行单向转换,以使密码可以安全地存储。

基于PasswordEncoder接口的实现类如下:

  • DelegatingPasswordEncoder:即委托密码编码器,兼容多种不同加密方式存储密码。主要用于新旧数据的加密方式的兼容,做到平滑迁移,例如旧数据使用NoOpPasswordEncoder,新数据使用BCryptPasswordEncoder加密。
  • BCryptPasswordEncoder:基于bcrypt算法的编码器,为了使其更能抵御密码破解,bcrypt故意降低了速度,与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。BCryptPasswordEncoder的默认实现使用强度10。
  • Argon2PasswordEncoder:基于 Argon2算法的编码器,Argon2是“密码哈希竞赛”的获胜者。 为了克服自定义硬件上的密码破解问题,Argon2是一种故意慢速的算法,需要大量内存。 与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。 Argon2PasswordEncoder的当前实现需要BouncyCastle。
  • Pbkdf2PasswordEncoder:基于 PBKDF2算法的编码器,为了克服密码破解问题,PBKDF2是一种故意缓慢的算法。 与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。 当需要FIPS认证时,此算法是一个不错的选择。
  • SCryptPasswordEncoder:基于 scrypt算法的编码器, 为了克服自定义硬件scrypt上的密码破解问题,这是一种故意缓慢的算法,需要大量内存。 与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。

防止漏洞利用

  • CSRF:Cross Site Request Forgery,即跨站请求伪造。Spring提供了两种机制来防御CSRF攻击:
    1、基于token表单验证(表单增加_csrf字段);
    2、在会话Cookie上指定SameSite属性;

  • 安全的HTTP响应头:

    • Cache Control:Spring Security的默认禁用缓存以保护用户的内容, Cache-Control: no-cache, no-store, max-age=0, must-revalidate
      Pragma: no-cache
      Expires: 0;
    • Content Type Options:禁用内容嗅探X-Content-Type-Options: nosniff防止XSS攻击;
    • HTTP Strict Transport Security (HSTS):严格的安全传输HTTP,将http请求自动转为https请求,Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload;
    • X-Frame-Options:Spring Security默认禁用iframe页面,X-Frame-Options: DENY;
    • X-XSS-Protection:通常浏览器在检测到XSS攻击时应采取的措施。 例如,过滤器可能会尝试以最小侵入性的方式更改内容以仍然呈现所有内容。 有时,这种替换本身可能会成为XSS漏洞。 相反,最好是阻止内容,而不要尝试对其进行修复。 Spring Security默认阻止内容:X-XSS-Protection: 1; mode=block;
    • Content Security Policy (CSP):内容安全策略是Web应用程序可以用来缓解诸如跨站点脚本(XSS)之类的内容注入漏洞的机制。
    • Referrer Policy:Referrer Policy: strict-origin-when-cross-origin;

一、认证过滤器

DelegatingFilterProxy

Spring提供了一个名为DelegatingFilterProxy的Filter实现,可以在Servlet容器的生命周期和Spring的ApplicationContext之间进行桥接。
Servlet容器允许使用自己的标准注册Filters,但是它不知道Spring定义的Bean。
DelegatingFilterProxy可以通过标准的Servlet容器机制进行注册,然后将所有工作委托给实现Filter的Spring Bean。

客户端到servlet的流程图

如上图所示,DelegatingFilterProxy是一个标准的Servlet Filter,当调用链路到DelegatingFilterProxy,DelegatingFilterProxy会找到达Spring管理的Filter,然后发起调用。

FilterChainProxy

FilterChainProxy是Spring Security提供的特殊过滤器,允许通过SecurityFilterChain委派许多过滤器实例。


FilterChainProxy

SecurityFilterChain

FilterChainProxy使用SecurityFilterChain确定应对此请求调用哪些Spring Security过滤器。

SecurityFilterChain

Security Filter是注册到FilterChainProxy而不是DelegatingFilterProxy的。原因如下:

  • 它为Spring Security的所有Servlet支持提供了一个起点。如果想对Spring Security的Servlet支持进行故障排除,那么在FilterChainProxy中添加调试点是一个很好的起点;
  • 它清除SecurityContext以避免内存泄漏;
  • 它使用Spring Security的HttpFirewall来保护应用程序免受某些类型的攻击;
  • 它在确定何时应调用SecurityFilterChain时提供了更大的灵活性。 在Servlet容器中,仅根据URL调用过滤器。 但是,FilterChainProxy可以利用RequestMatcher接口,根据HttpServletRequest中的任何内容确定调用。
  • FilterChainProxy可用于确定应使用哪个SecurityFilterChain。 这允许应用程序的不同部分提供完全独立的配置。

FilterChainProxy通过RequestMatcher接口来确定调用哪个SecurityFilterChain。
如下图所示, 将/api/**的请求与SecurityFilterChain0绑定

RequestMatcher的路由绑定

Security Filter

Spring Security提供了以下Security Filter(按照顺序罗列,顺序配置是通过FilterComparator):

  1. ChannelProcessingFilter
  2. WebAsyncManagerIntegrationFilter
  3. SecurityContextPersistenceFilter
  4. HeaderWriterFilter
  5. CorsFilter
  6. CsrfFilter
  7. LogoutFilter
  8. OAuth2AuthorizationRequestRedirectFilter
  9. Saml2WebSsoAuthenticationRequestFilter
  10. X509AuthenticationFilter
  11. AbstractPreAuthenticatedProcessingFilter
  12. CasAuthenticationFilter
  13. OAuth2LoginAuthenticationFilter
  14. Saml2WebSsoAuthenticationFilter
  15. UsernamePasswordAuthenticationFilter
  16. OpenIDAuthenticationFilter
  17. DefaultLoginPageGeneratingFilter
  18. DefaultLogoutPageGeneratingFilter
  19. ConcurrentSessionFilter
  20. DigestAuthenticationFilter
  21. BearerTokenAuthenticationFilter
  22. BasicAuthenticationFilter
  23. RequestCacheAwareFilter
  24. SecurityContextHolderAwareRequestFilter
  25. JaasApiIntegrationFilter
  26. RememberMeAuthenticationFilter
  27. AnonymousAuthenticationFilter
  28. OAuth2AuthorizationCodeGrantFilter
  29. SessionManagementFilter
  30. ExceptionTranslationFilter
  31. FilterSecurityInterceptor
  32. SwitchUserFilter

异常处理

异常处理过滤器ExceptionTranslationFilter的工作原理


ExceptionTranslationFilter的工作原理
  1. ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response),抛出Security Exception;
  2. 如果用户未通过身份验证或它是AuthenticationException,则启动身份验证:
    • 清除SecurityContextHolder;
    • 将HttpServletRequest保存在RequestCache中;
    • 进入AuthenticationEntryPoint(重定向到登录页或者返回401);
  3. 另外一种异常就是,访问被拒绝,则交给AccessDeniedHandler来处理。
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
 FilterChain chain, RuntimeException exception) throws IOException, ServletException {
 if (exception instanceof AuthenticationException) {
     handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
 }
 else if (exception instanceof AccessDeniedException) {
     handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
 }
 }

二、认证逻辑

认证相关组件

  • SecurityContextHolder:用于存储SecurityContext;

  • SecurityContext:用于存储Authentication;

  • Authentication:用户存储用户认证详细信息;

    认证相关类图

    上图为SecurityContextHolder、SecurityContext、Authentication三者之间的关系。

  • AuthenticationManager:统一管理执行身份验证;

  • ProviderManager:AuthenticationManager最常见的实现;

  • AuthenticationProvider:用于执行特定类型的身份验证,由ProviderManager统一管理。

    认证组件类的关系

ProviderManager作为AuthenticationManager最常见的实现,ProviderManager认证时,将认证逻辑委托给AuthenticationProvider列表,不同AuthenticationProvider的执行不同的认证逻辑。

ProviderManager认证委托

如果没有AuthenticationProvider可以执行身份验证,使用该父AuthenticationManager(通常是ProviderManager实例)进行认证。

AuthenticationManager父子类

一个父AuthenticationManager可能存在多个ProviderManager实例。 即具有相同身份验证(共享父AuthenticationManager)但又具有不同身份验证机制(不同ProviderManager实例)。

  • AuthenticationEntryPoint:认证入口,用户未登录时,重定向到登录页面或者发送401、WWW-Authenticate响应头;ExceptionTranslationFilter 抛出AuthenticationException的时候,调用sendStartAuthentication方法时进入AuthenticationEntryPoint逻辑。

  • AbstractAuthenticationProcessingFilter:作为身份验证的基本过滤器(formLogin表单登录)。 当用户提交身份凭据(例如,账号密码)时过滤。

    认证逻辑

上图表示用户提交身份凭据的认证逻辑:

  1. 假设用户在登录页提交表单(账号密码)进行登录;
  2. AbstractAuthenticationProcessingFilter判断该请求是提交身份凭据认证请求,则调用attemptAuthentication方法也就是上图中的①;
  3. attemptAuthentication将认证逻辑委托给AuthenticationManager就是上图中的②;
  4. 认证失败会做一些清理动作就是上图中的③;
  5. 认证成功,作Session相关的逻辑、将Authentication存储到SecurityContextHolder、RememberMe相关处理、认证成功事件的发布、认证成功的后置处理。
  • SessionAuthenticationStrategy:旨在身份验证时对HttpSession相关行为进行管理,如会话固定保护,csrf防护,session并发控制等。

    两种登录方式

如上图所示,两种登录方式:UsernamePasswordAuthenticationFilter(基于表单)与BasicAuthenticationFilter(基于Rest)。有两条请求链路:

  • 客户端请求经过UsernamePasswordAuthenticationFilter过滤器,验证用户登录凭证后,调用SessionAuthenticationStrategy.onAuthentication()基于session的验证;
  • 客户端请求经过BasicAuthenticationFilter过滤器,验证用户登录凭证后,需要再经过SessionManagementFilter过滤器,SessionManagementFilter接着将session验证委托给SessionAuthenticationStrategy。
SessionAuthenticationStrategy调用逻辑

上图SessionAuthenticationStrategy的调用逻辑:

  • 首先进入CompositeSessionAuthenticationStrategy,该策略意为混合session认证策略,它把session认证逻辑循环委托给其他具体的策略;
  • ConcurrentSessionControlAuthenticationStrategy,并发session控制,用于同一个用户多次登录管理,默认行为会剔除(标记过期SessionInformation.expireNow())最近最久没有使用的登录,maximumSessions=1配置一个账号最多几个登录共存;
  • ChangeSessionIdAuthenticationStrategy,顾名思义变更当前sessionId,目的为了会话固定防护,该类继承了AbstractSessionFixationProtectionStrategy。
  • RegisterSessionAuthenticationStrategy,为当前的新的sessionId注册一个SessionInformation(为了支持并发session控制);
  • CsrfAuthenticationStrategy,支持csrf防护,生成新的token。

讲到这里大家可能会有疑问,刚才提到SessionInformation是干么用的呢?
SessionInformation是为了支持session并发控制,ConcurrentSessionControlAuthenticationStrategy只是将SessionInformation标记为过期,等下次请求的时候会经过ConcurrentSessionFilter过滤器,此时会判断当前session对应的SessionInformation是否被标记过期了,如果是则调用session销毁动作,真正地登出。如果不是,得刷新更新SessionInformation的lastRequest(最近一次请求时间),ConcurrentSessionControlAuthenticationStrategy是基于该时间去剔除最近最久没有使用的登录。

认证机制

  • Username and Password
  • Remember Me
  • CAS
  • OAuth 2.0
  • SAML 2.0
  • JAAS Authentication
  • OpenID
  • Pre-Authentication Scenarios
  • X509 Authentication

Username and Password认证

验证用户身份的最常见方法之一是验证用户名和密码, Spring Security为使用用户名和密码进行身份验证提供了全面的支持。

认证方式
  • Form Login:基于表单登录认证;

    表单登录认证

上图表示未登录用户第一次访问web系统Spring Security处理流程:

  1. 用户在浏览器发起请求web系统私有资源 /private

  2. SecurityFilterChain过滤器链路到达FilterSecurityInterceptor,并抛出访问被拒绝的异常AccessDeniedException,ExceptionTranslationFilter捕获该异常并通过sendStartAuthentication方法进入LoginUrlAuthenticationEntryPoint(AuthenticationEntryPoint的实现类);

  3. LoginUrlAuthenticationEntryPoint设置了一个重定向到/login页面的响应头;

  4. 浏览器重定向到 /login,并获取到login.html,即登录主页面。

    验证流程

上图表示用户在表单提交用户名、密码的验证流程:

  1. 用户提交表单,进入UsernamePasswordAuthenticationFilter过滤器,通过调用attemptAuthentication方法将用户名、密码包装成UsernamePasswordAuthenticationToken对象,并传给AuthenticationManager;

    AuthenticationManager工作流程

上图表示AuthenticationManager工作流程:

  1. 将UsernamePasswordAuthenticationToken传给AuthenticationManager;

  2. ProviderManager将认证逻辑委托为DaoAuthenticationProvider;

  3. DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername方法获取UserDetails,通过PasswordEncoder比较用户请求传递过来UsernamePasswordAuthenticationToken上的密码与UserDetails存储的密码是否一致;

  4. 认证成功,将UserDetails作为principal,UserDetails的authorities作为authorities包装成UsernamePasswordAuthenticationToken返回。

  5. 认证失败会做一些清理动作;

  6. 认证成功,作Session相关的逻辑、将Authentication存储到SecurityContextHolder、RememberMe相关处理、认证成功事件的发布、认证成功的后置处理。

Basic Authentication:基础认证,基本HTTP REST的身份验证。

Basic Authentication

上图表示未登录用户第一次访问web系统Spring Security处理流程:

  1. 用户在浏览器发起请求web系统私有资源 /private

  2. SecurityFilterChain过滤器链路到达FilterSecurityInterceptor,并抛出访问被拒绝的异常AccessDeniedException,ExceptionTranslationFilter捕获该异常并通过sendStartAuthentication方法进入BasicAuthenticationEntryPoint(AuthenticationEntryPoint的实现类);

  3. BasicAuthenticationEntryPoint设置了WWW-Authenticate 响应头及
    401 响应码并返回。

    登录流程

上图表示当客户端收到WWW-Authenticate响应头时,使用用户名和密码登录的流程:

  1. 当用户提交其用户名和密码时,BasicAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken;
  2. 将UsernamePasswordAuthenticationToken传递到AuthenticationManager进行身份验证(这边的AuthenticationManager的认证逻辑与Form Login相同);
  3. 认证失败会做一些清理动作;
  4. 认证成功,将Authentication存储到SecurityContextHolder、RememberMe相关处理、调用FilterChain.doFilter(request,response)继续请求逻辑。
  • Digest Authentication:摘要式身份验证;不建议在现代应用程序中使用摘要式身份验证,因为它不安全。 它必须以纯文本,加密或MD5格式存储密码,这些存储格式都被认为是不安全的。不支持的单向自适应密码哈希(即bCrypt,PBKDF2,SCrypt等)存储凭据。

Remember-Me认证

Remember-Me(记住我),主要用于在一段很长的时间内(通常15天),用户只需要登录一次,就无需再登录了(前提是用户名、密码、秘钥不变的情况)。

原理:当用户登录成功时,服务端会向浏览器额外发送一个cookie(name = remember-me, value = token值),之后的请求都会携带这个cookie,当用户session失效时(比如2小时过后),该cookie携带到服务端触发自动登录。

当然,Remember-Me会存在一些安全问题,Remember-Me的token可以被用户代理捕获到,可以轻松通过该token去修改密码。因此在一些安全性重要的应用上面,不建议开启Remember-Me。

存储机制
  • In-Memory:内存存储;Spring Security的InMemoryUserDetailsManager实现了UserDetailsService,用来管理内存中的用户名/密码。
  • JDBC:数据库存储;使用JdbcUserDetailsManager基于JDBC的用户名/密码身份验证。
  • 自定义存储:使用UserDetailsService可自定义存储方式;例如:
// CustomUserDetailsService是UserDetailsService的实现类
@Bean
CustomUserDetailsService customUserDetailsService() {
 return new CustomUserDetailsService();
}
  • LDAP:LDAP存储,有兴趣可以自行查阅官方文档;

三、授权原理分析(Servlet)

FilterSecurityInterceptor授权

FilterSecurityInterceptor为HttpServletRequests提供授权,它作为安全筛选器之一插入到FilterChainProxy中(除此之外,Spring Security支持服务层方法授权还有域对象授权)。

FilterSecurityInterceptor授权
  1. FilterSecurityInterceptor可以从SecurityContextHolder获取Authentication;
  2. FilterSecurityInterceptor将HttpServletRequest、HttpServletResponse、FilterChain包装成FilterInvocation对象,调用父类AbstractSecurityInterceptor.beforeInvocation(),进入前置处理阶段;
  3. AbstractSecurityInterceptor从SecurityMetadataSource获取配置信息 ConfigAttribute(例如:hasRole('ROLE_USER'));调用AbstractSecurityInterceptor.authenticateIfRequired()执行认证相关逻辑;
  4. 调用AccessDecisionManager.decide()方法,进行访问决策处理,该方法将决策委托给AccessDecisionVoter访问决策投票器,进行投票处理,AccessDecisionManager将投票的结果进行整合;
  5. 如果AccessDecisionManager决策被拒绝,则抛出AccessDeniedException,ExceptionTranslationFilter捕获到这个异常,并交给AuthenticationEntryPoint进行相应的处理,例如跳转登录页、返回403状态等;
  6. 如果AccessDecisionManager决策成功,则方法正常调用chain.doFilter();
  7. 方法正常调用完成后,通过AbstractSecurityInterceptor.afterInvocation()进入后置处理阶段(某些应用程序需要修改实际返回的对象);

Authorities权限列表

image

回顾下前面学到的知识,用户登录认证成功后,会为当前用户生成一个Authentication对象,该对象包含了Principal、Credentials和Authorities;
该Authorities由GrantedAuthority(默认实现:SimpleGrantedAuthority)集合组成,GrantedAuthority接口只有一个方法,如下:

String getAuthority();

一般情况下GrantedAuthority由String表示,如果GrantedAuthority无法精确地表示为String,则GrantedAuthority被视为“复杂”,并且getAuthority()必须返回null。

前置处理阶段

刚刚讲到Spring Security调用AbstractSecurityInterceptor.beforeInvocation()进入前置处理阶段,该阶段的一个重点就是进行访问决策处理,由AccessDecisionManager相关实现来完成,AccessDecisionManager接口包含三个方法:

// 决策处理逻辑
void decide(Authentication authentication, Object secureObject,
    Collection<ConfigAttribute> attrs) throws AccessDeniedException;
// 项目启动校验配置是否正确
boolean supports(ConfigAttribute attribute);
// 项目启动校验配置是否正确
boolean supports(Class clazz);
image

如上图所示,Spring Security提供了三个决策处理器AccessDecisionManager的实现类(AffirmativeBased、ConsensusBased、UnanimousBased),代表三种不同的决策处理器,当然也可以自定义决策处理器。决策处理器将决策逻辑委托给多个投票器AccessDecisionVoter(具体实现有:AuthenticatedVoter、RoleVoter、WebExpressionVoter等),接着AccessDecisionManager将投票结果进行整合,返回拒绝或者成功。

AccessDecisionManager实现类的具体描述如下:

  • AffirmativeBased:只要有一票通过,返回成功;
  • ConsensusBased:通过票数 > 不通过票数,返回成功;如果通过票数 = 不通过票数,这个时候配置allowIfEqualGrantedDeniedDecisions=true,即可返回成功;
  • UnanimousBased:与上面两个不同的是,UnanimousBased需要轮询每个ConfigAttribute,然后投票器对每个ConfigAttribute进行投票,只要有一票不通过,则返回失败。

AccessDecisionVoter通过返回int来表示投票的结果,有ACCESS_ABSTAIN(0,弃权),ACCESS_DENIED(-1,不通过)和ACCESS_GRANTED(1,通过),AccessDecisionVoter主要的实现类如下:

  • RoleVoter:角色投票器,最常用的投票器。如果任何ConfigAttribute以前缀ROLE_开头,它将进行投票,否则投票者将弃权ACCESS_ABSTAIN,如果存在GrantedAuthority可以返回一个字符串表示形式(通过getAuthority()方法),该字符串表示形式完全等于一个或多个以前缀ROLE_开头的ConfigAttributes,则它将投票通过ACCESS_GRANTED,否则投票不通过ACCESS_DENIED。
  • AuthenticatedVoter:用来区分匿名,完全认证和记住我的认证用户。 许多站点允许使用“记住我”身份验证,但是某些资源确要求完全认证方式(用户登录)才能访问。
  • WebExpressionVoter:web表达式投票器,基于Spring EL进行解析,例如:hasRole('ROLE_USER')。表达式根对象的基类是SecurityExpressionRoot,提供了Web和方法安全性中都可用的一些常用表达式。详情参考:https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/#el-access

后置处理阶段

某些应用程序需要一种修改返回的对象的方法,因此Spring Security提供了一个方便的挂钩AfterInvocationManager,通过AfterInvocationManager来修改返回对象。

image

如上图所示,AfterInvocationManager有一个具体的实现AfterInvocationProviderManager,它轮询AfterInvocationProvider的列表。 每个AfterInvocationProvider都可以修改返回对象或引发AccessDeniedException。 实际上,由于前一个提供程序的结果将传递到列表中的下一个,因此多个提供程序可以修改该对象。

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

推荐阅读更多精彩内容