springSecurity深度解析第二版

作者:muggle

前言

由于第一版排版实在太过糟糕,而且很多细节没交代清楚,所以决定写第二版;这一版争取将排版设计得清晰明了一点,以方便读者阅读。

security原理分析

springSecurity过滤器链

springSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明

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

  2. SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder中的信息清除
    例如在Session中维护一个用户的安全信息就是这个过滤器处理的。

  3. HeaderWriterFilter:用于将头信息加入响应中

  4. CsrfFilter:用于处理跨站请求伪造

  5. LogoutFilter:用于处理退出登录

  6. UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自“/login”的请求。从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。

  7. DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

  8. BasicAuthenticationFilter:检测和处理http basic认证

  9. RequestCacheAwareFilter:用来处理请求的缓存

  10. SecurityContextHolderAwareRequestFilter:主要是包装请求对象request

  11. AnonymousAuthenticationFilter:检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication

  12. SessionManagementFilter:管理session的过滤器

  13. ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常

  14. FilterSecurityInterceptor:可以看做过滤器链的出口

  15. RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从cookie里找出用户的信息, 如果Spring Security能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

springSecurity 流程图

上一版是通过debug的方法告诉读者springSecurity的一个执行过程,发现反而把问题搞复杂了,这一版我决定画一个流程图来说明其执行过程,只要把springSecurity的执行过程弄明白了,这个框架就会变得很简单

security.png

流程说明

  1. 客户端发起一个请求,进入security过滤器链;

  2. 当到LogoutFilter的时候判断是否是登出路径,如果是登出路径则到logoutHandler,如果登出成功则到logoutSuccessHandler登出成功处理,如果登出失败则由ExceptionTranslationFilter;如果不是登出路径则直接进入下一个过滤器;

  3. 当到UsernamePasswordAuthenticationFilter的时候判断是否为登陆路径,如果是,则进入该过滤器进行登陆操作,如果登陆失败则到AuthenticationFailureHandler登陆失败处理器处理,如果登陆成功则到AuthenticationSuccessHandler登陆成功处理器处理 ;如果不是登陆请求则不进入该过滤器

  4. 当到FilterSecurityInterceptor的时候会拿到urI,根据uri去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到controller层否则到AccessDeniedHandler鉴权失败处理器处理

security配置

WebSecurityConfigurerAdapter这个类里面可以完成上述流程图的所有配置

配置类伪代码

 **/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
    }
    @Override
    public void configure(WebSecurity web) throws Exception {

        web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       http.formLogin().loginPage("/login_page").passwordParameter("username").passwordParameter("password").loginProcessingUrl("/sign_in").permitAll()
               .and().authorizeRequests().antMatchers("/test").hasRole("test")
               .anyRequest().authenticated().accessDecisionManager(accessDecisionManager())
               .and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
               .and().csrf().disable();
        http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
        http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
        http.addFilterAfter(new MyFittler(), LogoutFilter.class);
    }
}

配置类说明

configure(AuthenticationManagerBuilder auth) 说明

AuthenticationManager的建造器,配置AuthenticationManagerBuilder 会让security自动构建一个AuthenticationManager(该类的功能参考流程图);如果想要使用该功能你需要配置一个UserDetailService和passwordEncoder。userDetailsService用于在认证器中根据用户传过来的用户名查找一个用户,passwordEncoder用于密码的加密与比对,我们存储用户密码的时候用passwordEncoder.encode()加密存储,在认证器里会调用passwordEncoder.matches()方法进行密码比对。

如果重写了该方法,security会启用DaoAuthenticationProvider这个认证器,该认证就是先调用UserDetailsService.loadUserByUsername然后使用passwordEncoder.matches()进行密码比对,如果认证成功成功则返回一个Authentication对象

configure(WebSecurity web)说明

这个配置方法用于配置静态资源的处理方式,可使用ant匹配规则

configure(HttpSecurity http) 说明

这个配置方法是最关键的方法,也是最复杂的方法。我们慢慢掰开来说

http.formLogin().loginPage("/login_page").passwordParameter("username").passwordParameter("password").loginProcessingUrl("/sign_in").permitAll()

这是配置登陆相关的操作从方法名可知,配置了登录页请求路径,密码属性名,用户名属性名,和登陆请求路径,permitAll()代表任意用户可访问

http.authorizeRequests().antMatchers("/test").hasRole("test").anyRequest().authenticated().accessDecisionManager(accessDecisionManager());

以上配置是权限相关的配置,配置了一个“/test” url该有什么权限才能访问,anyRequest()表示所有请求,authenticated()表示已登录用户,accessDecisionManager()表示绑定在url上的鉴权管理器

为了对比,现在贴出另一个权限配置清单

http.authorizeRequests().antMatchers("/tets_a/**","/test_b/**").hasRole("test").antMatchers("/a/**","/b/**").authenticated().accessDecisionManager(accessDecisionManager())

我们可以看到权限配置的自由度很高,鉴权管理器可以绑定到任意url上;而且可以硬编码各种url权限;

http.logout().logoutUrl("/logout").logoutSuccessHandler(new MyLogoutSuccessHandler())

登出相关配置,这里配置了登出url和登出成功处理器

http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());

上面代码是配置鉴权失败的处理器

http.addFilterAfter(new MyFittler(), LogoutFilter.class);
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

上面代码展示如何在过滤器链中插入自己的过滤器,addFilterBefore加在对应的过滤器之前addFilterAfter之后,addFilterAt加在过滤器同一位置,事实上框架原有的Filter在启动HttpSecurity配置的过程中,都由框架完成了其一定程度上固定的配置,是不允许更改替换的。根据测试结果来看,调用addFilterAt方法插入的Filter,会在这个位置上的原有Filter之前执行。

注:关于HttpSecurity使用的是链式编程,其中http.xxxx.and.yyyyy这种写法和http.xxxx;http.yyyy写法意义一样。

自定义authenticationManager和accessDecisionManager

重写authenticationManagerBean()方法,并构造一个authenticationManager

@Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(getMyAuthenticationProvider(),daoAuthenticationProvider()));
        return authenticationManager;
    }

我这里给authenticationManager配置了两个认证器,执行过程参考流程图

定义构造AccessDecisionManager的方法并在配置类中调用,配置参考 configure(HttpSecurity http) 说明

public AccessDecisionManager accessDecisionManager(){
        List<AccessDecisionVoter<? extends Object>> decisionVoters
                = Arrays.asList(
                new MyExpressionVoter(),
                new WebExpressionVoter(),
                new RoleVoter(),
                new AuthenticatedVoter());
        return new UnanimousBased(decisionVoters);

    }

投票管理器会收集投票器投票结果做统计,最终结果大于等于0代表通过;每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。

security 权限用户系统说明

UserDetails

security中的用户接口,我们自定义用户类要实现该接口,各个属性的含义自行百度

GrantedAuthority

security中的用户权限接口,自定义权限需要实现该接口

@Data
public class MyGrantedAuthority implements GrantedAuthority {
    private String authority;
}

authority权限字段,需要注意的是在config中配置的权限会被加上ROLE_前缀,比如我们的配置authorizeRequests().antMatchers("/test").hasRole("test"),配置了一个“test”权限但我们存储的权限字段(authority)应该是“ROLE_test”

UserDetailsService

security用户service,自定义用户服务类需要实现该接口

@Service
public class MyUserDetailService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
      return.....
    }
}

loadUserByUsername的作用在上文中已经说明;

SecurityContextHolder

用户在完成登陆后security会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得,用户信息被封装成SecurityContext ,而实际存储的类是SecurityContextHolderStrategy ,默认的SecurityContextHolderStrategy 实现类是ThreadLocalSecurityContextHolderStrategy 它使用了ThreadLocal来存储了用户信息。

手动填充SecurityContextHolder示例:

UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
SecurityContextHolder.getContext().setAuthentication(token);

对于token鉴权的系统

我们就可以验证token后手动填充SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空SecurityContextHolder。

security扩展说明

可扩展的有

  • 鉴权失败处理器:security鉴权失败默认跳转登陆页面,我们可以
  • 验证器
  • 登陆成功处理器
  • 投票器
  • 自定义token处理过滤器
  • 登出成功处理器
  • 登陆失败处理器
  • 自定义UsernamePasswordAuthenticationFilter

鉴权失败处理器

security鉴权失败默认跳转登陆页面,我们可以实现AccessDeniedHandler接口,重写handle()方法来自定义处理逻辑;然后参考配置类说明将处理器加入到配置当中

验证器

实现AuthenticationProvider接口来实现自己验证逻辑。需要注意的是在这个类里面就算你抛出异常,也不会中断验证流程,而是算你验证失败,我们由流程图知道,只要有一个验证器验证成功,就算验证成功,所以你需要留意这一点

登陆成功处理器

在security中验证成功默认跳转到上一次请求页面或者路径为"/"的页面,我们同样可以自定义:继承SimpleUrlAuthenticationSuccessHandler这个类或者实现AuthenticationSuccessHandler接口。我这里建议采用继承的方式;SimpleUrlAuthenticationSuccessHandler是默认的处理器,采用继承可以契合里氏替换原则,提高代码的复用性和避免不必要的错误。

投票器

投票器可继承WebExpressionVoter或者实现AccessDecisionVoter<FilterInvocation>接口;WebExpressionVoter是security默认的投票器;我这里同样建议采用继承的方式;添加到配置的方式参考 配置类说明章节;

注意:投票器vote方法返回一个int值;-1代表反对,0代表弃权,1代表赞成;投票管理器收集投票结果,如果最终结果大于等于0则放行该请求。

自定义token处理过滤器

自定义token处理器继承自可OncePerRequestFilter或者GenericFilterBean或者Filter都可以,在这个处理器里面需要完成的逻辑是:获取请求里的token,验证token是否合法然后填充SecurityContextHolder,虽然说过滤器只要添加在投票器之前就可以;但我这里还是建议添加在http.addFilterAfter(new MyFittler(), LogoutFilter.class);

登出成功处理器

实现LogoutSuccessHandler接口,添加到配置的方式参考 配置类说明章节

登陆失败处理器

登陆失败默认跳转到登陆页,我们同样可以自定义。继承SimpleUrlAuthenticationFailureHandler 或者实现AuthenticationFailureHandler;建议采用继承。

自定义UsernamePasswordAuthenticationFilter

我们自定义UsernamePasswordAuthenticationFilter可以极大提高我们security的灵活性(比如添加验证验证码是否正确的功能),所以我这里是建议自定义UsernamePasswordAuthenticationFilter;

我们直接继承UsernamePasswordAuthenticationFilter,然后在配置类中初始化这个过滤器,给这个过滤器添加登陆失败处理器,登陆成功处理器,登陆管理器,登陆请求url

这里配置略微复杂,贴一下代码清单

初始化过滤器:

MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){
        MyUsernamePasswordAuthenticationFilte myUsernamePasswordAuthenticationFilte = new MyUsernamePasswordAuthenticationFilte(redisService);
        myUsernamePasswordAuthenticationFilte.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());
        myUsernamePasswordAuthenticationFilte.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        myUsernamePasswordAuthenticationFilte.setFilterProcessesUrl("/sign_in");
        myUsernamePasswordAuthenticationFilte.setAuthenticationManager(getAuthenticationManager());
        return myUsernamePasswordAuthenticationFilte;
    }

添加到配置:

http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

代码清单

下面贴出适配于 前后端分离和token验证的伪代码清单

登陆页请求处理

@Controller
public class LoginController {
    /** 
    * @Description: 登陆页面的请求 
    * @Param:  
    * @return:  
    */ 
    @GetMapping("/login_page")
    public String loginPage(){
        return "loginPage.html";
    }
}

鉴权失败处理器

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write("{\"code\":\"403\",\"msg\":\"没有权限\"}");
        writer.close();
    }
}

验证器

public class MyAuthenticationProvider  implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public MyAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//       这里写验证逻辑
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

验证成功处理器

ublic class MyAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //随便写点啥
    }
}

投票器

/**
 * @program: security-test
 * @description: 鉴权投票器
 * @author: muggle
 * @create: 2019-04-11
 **/

public class MyExpressionVoter extends WebExpressionVoter {
    @Override
    public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
//        这里写鉴权逻辑
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        return 1 ;
    }
}

自定义token处理过滤器

/**
 * @program: security-about
 * @description:填充一个token
 * @author: muggle
 * @create: 2019-04-20
 **/

public class MyFittler extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token1 = request.getHeader("token");
        if (token1==null){

        }

        ArrayList<GrantedAuthority> list = new ArrayList<>();
        GrantedAuthority grantedAuthority = new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return "test";
            }
        };
        list.add(grantedAuthority);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(request, response);
    }
}

登出成功处理器

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        final PrintWriter writer = response.getWriter();

        writer.write("{\"code\":\"200\",\"msg\":\"登出成功\"}");
        writer.close();
    }
}

登陆失败处理器

public class MyUrlAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        final PrintWriter writer = response.getWriter();
        if(exception.getMessage().equals("坏的凭证")){
            writer.write("{\"code\":\"401\",\"msg\":\"登录失败,用户名或者密码有误\"}");
            writer.close();
        }else {
            writer.write("{\"code\":\"401\",\"msg\":\"登录失败,"+exception.getMessage()+"\"}");
            writer.close();
        }

    }
}

自定义UsernamePasswordAuthenticationFilter

/**
 * @program: security-test
 * @description: 用户登陆逻辑过滤器
 * @author: muggle
 * @create: 2019-04-11
 **/

public class MyUsernamePasswordAuthenticationFilte extends UsernamePasswordAuthenticationFilter {
    private RedisService redisService;
    private boolean postOnly = true;

    public MyUsernamePasswordAuthenticationFilte(RedisService redisService){
        this.redisService=redisService;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
     
        //你可以在这里做验证码校验,校验不通过抛出AuthenticationException()即可
            super.attemptAuthentication(request,response);
    }
}

配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    RedisService redisService;
    @Autowired
    MyUserDetailService userDetailService;
  
    @Override
    public void configure(WebSecurity web) throws Exception {

        web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js",
                "/resources/**/*.css", "/resources/**/*.txt",
                "/resources/**/*.png", "/**/*.bmp", "/**/*.gif", "/**/*.png", "/**/*.jpg", "/**/*.ico");
//        super.configure(web);
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        配置登录页等 permitAll表示任何权限都能访问
       http.formLogin().loginPage("/login_page").passwordParameter("username").passwordParameter("password").loginProcessingUrl("/sign_in").permitAll()
               .and().authorizeRequests().antMatchers("/test").hasRole("test")
//               任何请求都被accessDecisionManager() 的鉴权器管理
               .anyRequest().authenticated().accessDecisionManager(accessDecisionManager())
//               登出配置
               .and().logout().logoutUrl("/logout").logoutSuccessHandler(new MyLogoutSuccessHandler())
//               关闭csrf
               .and().csrf().disable();
       http.authorizeRequests().antMatchers("/tets_a/**","/test_b/**").hasRole("test").antMatchers("/a/**","/b/**").authenticated().accessDecisionManager(accessDecisionManager())
//      加自定义过滤器
        http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
//        配置鉴权失败的处理器
        http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
        http.addFilterAfter(new MyFittler(), LogoutFilter.class);

    }


    MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){
        MyUsernamePasswordAuthenticationFilte myUsernamePasswordAuthenticationFilte = new MyUsernamePasswordAuthenticationFilte(redisService);
        myUsernamePasswordAuthenticationFilte.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());
        myUsernamePasswordAuthenticationFilte.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        myUsernamePasswordAuthenticationFilte.setFilterProcessesUrl("/sign_in");
        myUsernamePasswordAuthenticationFilte.setAuthenticationManager(getAuthenticationManager());
        return myUsernamePasswordAuthenticationFilte;
    }
    MyAuthenticationProvider getMyAuthenticationProvider(){
        MyAuthenticationProvider myAuthenticationProvider = new MyAuthenticationProvider(userDetailService,new BCryptPasswordEncoder());
        return myAuthenticationProvider;
    }
    DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        daoAuthenticationProvider.setUserDetailsService(userDetailService);
        return daoAuthenticationProvider;
    }
    protected AuthenticationManager getAuthenticationManager()  {
        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(getMyAuthenticationProvider(),daoAuthenticationProvider()));
        return authenticationManager;
    }

    public AccessDecisionManager accessDecisionManager(){
        List<AccessDecisionVoter<? extends Object>> decisionVoters
                = Arrays.asList(
                new MyExpressionVoter(),
                new WebExpressionVoter(),
                new RoleVoter(),
                new AuthenticatedVoter());
        return new UnanimousBased(decisionVoters);

    }
}

总结

对于security的扩展配置关键在于configure(HttpSecurity http)方法;扩展认证方式可以自定义authenticationManager并加入自己验证器,在验证器中抛出异常不会终止验证流程;扩展鉴权方式可以自定义accessDecisionManager然后添加自己的投票器并绑定到对应的url(url 匹配方式为ant)上,投票器vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes)方法返回值为三种:-1 0 1,分别表示反对弃权赞成;

对于token认证的校验方式,可以暴露一个获取的接口,或者重写UsernamePasswordAuthenticationFilter过滤器和扩展登陆成功处理器来获取token,然后在LogoutFilter之后添加一个自定义过滤器,用于校验和填充SecurityContextHolder

security的处理器大部分都是重定向的,我们的项目如果是前后端分离的话,我们希望无论什么情况都返回json,那么就需要重写各个处理器了。

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

推荐阅读更多精彩内容