spring boot 整合 spring security4

我们在编写Web应用时,经常需要对页面做一些安全控制,比如:对于没有访问权限的用户需要转到登录表单页面。要实现访问控制的方法多种多样,可以通过Aop、拦截器实现,也可以通过框架实现(如:Apache Shiro、Spring Security)。

本文将具体介绍在Spring Boot中如何使用Spring Security进行安全控制。
整体框架: spring boot spring data jpa spring security

心得

在整理当前框架时,遇到了几个问题

<sec:authorize access="hasRole('ROLE_USER')">
     这里是角色ROLE_USER可以看到
</sec:authorize> 
<sec:authorize url="/admin">
   这里是具有 /admin 资源的用户可以看到
</sec:authorize>

当时官网是这样描述着两个标签的
此标记用于确定是否应评估其内容。在spring 3.0,它可以以两种方式使用 。第一种方法使用了网络的安全性表达,在指定access标签的属性。表达式求值将被委托给SecurityExpressionHandler<FilterInvocation>应用程序上下文中定义(你应该在你的基于Web的表达<http>空间配置,以确保该服务可用)。所以,例如,你可能有<sec:authorize access="hasRole('ROLE_USER')"></sec:authorize>这这种标签可以直接使用 .
但是对于 URL 来讲就没那么简单了.需要自定义DefaultWebInvocationPrivilegeEvaluator类. 下面我会给出详细设计代码,在这之前我想多说一句,当时扩展的时候我遇到了标签不起作用,百度 谷歌了好久,也没有解决问题.我在群里问人的时候,群里的回答也是让我大写的服...一个个的都不认字吗?
有人回答说用 shiro 吧....有人回答说,谁还用 JSP... 有人回答说,自定义标签吧...有人回答说,用 hasrole 标签吧... url 没用....我真是服了,,我求求你们,你们是怎么当上程序员的啊!!!!!当别人问你们问题的时候,,你们的回答也是大写的服!!!!!! 别人用 jsp 咋了,,跟当前问的问题有任何关系吗?所以啊有什么问题还是靠自己解决啊.. 于是就跟踪源代码DefaultWebInvocationPrivilegeEvaluator. java中有个securityInterceptor属性.这个属性就决定是用扩展自定义的类还是用 springsecurity 本身自己的类...最后发现是我这个地方没有注入进去..查询了官方 API, 原来发现 javaconfig 的方式在在

 public void configure(WebSecurity web) throws Exception {
       web.securityInterceptor(myFilterSecurityInterceptor);
       web.privilegeEvaluator(customWebInvocationPrivilegeEvaluator());
}

这样才能做到注入自己的扩展的FilterSecurityInterceptor,下面我会给出详细代码.
参考文档 http://docs.spring.io/spring-security/site/docs/4.2.2.BUILD-SNAPSHOT/reference/htmlsingle/
http://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/config/annotation/web/builders/WebSecurity.html
解决问题还是得靠自己. 多看文档,多跟踪源代码,多看 API. 下面开始进入正题.

表设计

springsecurity框架的表设计还是很简单的, user 用户表, role 角色表, resource 资源表.然后三者通过关系关联,我这里设计了5张表, user,role,resource,user_role,role_resource 其中user_role表是用户与角色之间的关系,多对多,role_resource 关系也是这样.

实体类

user.java
@Entity
@Table(name = "ad_operator_info")
public class User extends BaseEntity {
   /**
    * 主键
    */
   @Id
   @GeneratedValue(generator = "uuid")
   @GenericGenerator(name = "uuid", strategy = "uuid")
   @Column(name = "oper_id", length = 32)
   private String operId;
   /**
    * 用户名
    */
   @Column(name = "user_name")
   private String userName;
   /**
    * 密码
    */
   private String password;

   @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
   @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
   private Set<Role> roles;

   //省略 get... set..
}
role.java
@Entity
@Table(name = "ad_role")
public class Role extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
    private Set<OperatorInfo> users;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(name = "ad_roles_resources", joinColumns = {@JoinColumn(name = "rid")}, inverseJoinColumns = {@JoinColumn(name = "eid")})
    private Set<Resource> resources;
   // 省略 get set
}
Resource.java
@Entity
@Table(name = "ad_web_resource")
public class WebResource extends BaseEntity {

    private static final long serialVersionUID = 7926081201477024763L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // 主键

    private String name; // 资源名称

    private String url;

    @Column(name="remark",length=200)
    private String remark;//备注

    @Column(name="methodName",length=400)
    private String methodName;//资源所对应的方法名

    @Column(name="methodPath",length=1000)
    private String methodPath;//资源所对应的包路径

    private String sn;

    private String value; // 资源标识
   
   // 省略 get set  这里的属性可以根据自己的业务来.
}

实体类就此准备完毕. 下面加入 springsecurity 的 jar 包

  1. 下载 jar
<parent>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-parent</artifactId>  
        <version>1.4.1.RELEASE</version>  
    </parent>  
    <dependencies>  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-web</artifactId>  
        </dependency>  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-security</artifactId>  
        </dependency>  
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
            <version>4.2.1.RELEASE</version>
        </dependency>
</dependencies>  

2 .Spring Security配置
创建Spring Security的配置类 WebSecurityConfig,也是注入自己定义扩展FilterSecurityInterceptor的重要类 ,具体如下:

import com.pwkj.potevio.adp.auth.MyFilterSecurityInterceptor;
import com.pwkj.potevio.adp.auth.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
 * Created by PrimaryKey on 17/2/4.
 *
 * @EnableWebSecurity: 禁用Boot的默认Security配置,配合@Configuration启用自定义配置(需要扩展WebSecurityConfigurerAdapter)
 * @EnableGlobalMethodSecurity(prePostEnabled = true): 启用Security注解,例如最常用的@PreAuthorize
 * configure(HttpSecurity): Request层面的配置,对应XML Configuration中的<http>元素
 * configure(WebSecurity): Web层面的配置,一般用来配置无需安全检查的路径
 * configure(AuthenticationManagerBuilder): 身份验证配置,用于注入自定义身份验证Bean和密码校验规则
 */


@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailService myUserDetailService;
    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;

   @Bean
    @Primary
    public DefaultWebInvocationPrivilegeEvaluator customWebInvocationPrivilegeEvaluator() {
        return new DefaultWebInvocationPrivilegeEvaluator(myFilterSecurityInterceptor);
    }
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // javaconfig 配置是这样 set 进去的.
        web.securityInterceptor(myFilterSecurityInterceptor);
        web.privilegeEvaluator(customWebInvocationPrivilegeEvaluator());
        web.
                ignoring()
                .antMatchers("/assets/**", "/login", "/login/success", "/kaptcha/**", "/**/*.jsp");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/resources", "/login", "/kaptcha/**").permitAll()//访问:这些路径 无需登录认证权限
                .anyRequest().authenticated() //其他所有资源都需要认证,登陆后访问
                //.antMatchers("/resources").hasAuthority("ADMIN") //登陆后之后拥有“ADMIN”权限才可以访问/hello方法,否则系统会出现“403”权限不足的提示
         .and()
                .formLogin()
                .loginPage("/")//指定登录页是”/”
                .permitAll()
                .successHandler(loginSuccessHandler()) //登录成功后可使用loginSuccessHandler()存储用户信息,可选。
         .and()
                .logout()
                .logoutUrl("/admin/logout")
                .logoutSuccessUrl("/") //退出登录后的默认网址是”/home”
                .permitAll()
                .invalidateHttpSession(true);
               // .and()
                //.rememberMe()//登录后记住用户,下次自动登录,数据库中必须存在名为persistent_logins的表
                //.tokenValiditySeconds(1209600);
        http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //指定密码加密所使用的加密器为passwordEncoder()
        //需要将密码加密后写入数据库
        auth.userDetailsService(myUserDetailService);//.passwordEncoder(bCryptPasswordEncoder());
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(4);
    }

    @Bean
    public LoginSuccessHandler loginSuccessHandler() {
        return new LoginSuccessHandler();
    }
}

编写LoginSuccessHandler.java 此类是在登陆成功之后做一些业务操作

package com.pwkj.potevio.adp.config;

import com.pwkj.potevio.adp.entity.OperatorInfo;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Created by PrimaryKey on 17/2/4.
 */
public class LoginSuccessHandler extends
        SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication) throws IOException,
            ServletException {
        //获得授权后可得到用户信息   可使用OperatorInfoService进行数据库操作
        OperatorInfo userDetails = (OperatorInfo) authentication.getPrincipal();
       /* Set<SysRole> roles = userDetails.getSysRoles();*/
        //输出登录提示信息
        System.out.println("管理员 " + userDetails.getName() + " 登录");

        System.out.println("IP :" + getIpAddress(request));

        super.onAuthenticationSuccess(request, response, authentication);
    }


    public String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

下面是自定义的过滤器,也是最重要的集成代码.
首先编写 MyInvocationSecurityMetadataSource.java 此类是首先加载的,用于加载资源配置.用resourceMap对象存储url --> value

package com.pwkj.potevio.adp.auth;

/**
 * Created by PrimaryKey on 17/2/4.
 */

import com.pwkj.potevio.adp.dao.WebResourceDao;
import com.pwkj.potevio.adp.entity.WebResource;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.*;

@Service
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
    private org.slf4j.Logger LOG = LoggerFactory.getLogger(getClass());

    @Autowired
    private WebResourceDao webResourceDao;

    /**
     * 加载资源,初始化资源变量
     */
    @PostConstruct
    public void loadResourceDefine() {
        if (resourceMap == null) {
            resourceMap = new HashMap<String, Collection<ConfigAttribute>>();
            List<WebResource> resources = webResourceDao.findAll();
            for (WebResource resource : resources) {
                Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
                ConfigAttribute configAttribute = new SecurityConfig(resource.getValue());
                configAttributes.add(configAttribute);
                resourceMap.put(resource.getUrl(), configAttributes);
            }
        }
        LOG.info("security info load success!!");
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        if (resourceMap == null) loadResourceDefine();
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
       // 返回当前 url  所需要的权限
         return resourceMap.get(requestUrl);
    }



    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

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

其次编写 MyUserDetailService.java 此类用来获取用户的所有权限.

package com.pwkj.potevio.adp.auth;

import com.pwkj.potevio.adp.entity.OperatorInfo;
import com.pwkj.potevio.adp.entity.Role;
import com.pwkj.potevio.adp.entity.WebResource;
import com.pwkj.potevio.adp.service.OperatorInfoService;
import com.pwkj.potevio.adp.service.WebResourceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * Created by PrimaryKey on 17/2/4.
 * 二
 */
@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private OperatorInfoService operatorInfoService;

    @Autowired
    private WebResourceService webResourceService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //取得用户
        OperatorInfo operatorInfo = operatorInfoService.findByUserName(userName);
        if (operatorInfo == null) {
            throw new UsernameNotFoundException("UserName " + userName + " not found");
        }
        // 取得用户的权限
        Collection<GrantedAuthority> grantedAuths = obtionGrantedAuthorities(operatorInfo);
        Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
        for (Role role : operatorInfo.getRoles()) {
            grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        // 封装成spring security的user
        User userDetail = new User(operatorInfo.getUserName(), operatorInfo.getPassword(),
                true,//是否可用
                true,//是否过期
                true,//证书不过期为true
                true,//账户未锁定为true ,
                grantedAuths);
        return userDetail;
    }

    // 取得用户的权限
    private Set<GrantedAuthority> obtionGrantedAuthorities(OperatorInfo operatorInfo) {
        List<WebResource> resources = new ArrayList<WebResource>();
        //获取用户的角色
        Set<Role> roles = operatorInfo.getRoles();
        for (Role role : roles) {
            Set<WebResource> res = role.getResources();
            for (WebResource resource : res) {
                resources.add(resource);
            }
        }
        Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
        for (WebResource r : resources) {
            //用户可以访问的资源名称(或者说用户所拥有的权限)
            authSet.add(new SimpleGrantedAuthority(r.getValue()));
        }
        return authSet;
    }
}
```
再次编写 ```MyFilterSecurityInterceptor.java``` 用于跳转
```
package com.pwkj.potevio.adp.auth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;

import javax.servlet.*;
import java.io.IOException;

/**
 * Created by PrimaryKey on 17/2/4.
 *
 * 三
 */
@Service
public class MyFilterSecurityInterceptor extends FilterSecurityInterceptor implements Filter {

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        invoke(fi);
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        //fi里面有一个被拦截的url
        //里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
        //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            //执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

```
最后编写```MyAccessDecisionManager.java``` 类用来判断当前用户是否有访问权限.
```
package com.pwkj.potevio.adp.auth;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.Iterator;

/**
 * Created by PrimaryKey on 17/2/4.
 *
 * 最后一个类
 */
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {


        // TODO  权限 .... >>>
        if (configAttributes == null) {
            return;
        }
        //所请求的资源拥有的权限(一个资源对多个权限)
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            //访问所请求资源所需要的权限
            String needPermission = configAttribute.getAttribute();
             //用户所拥有的权限authentication
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                System.out.println("-----------PrimaryKey-----------ga.getAuthority()值=" + ga.getAuthority() + "," + "当前类=MyAccessDecisionManager.decide()");
                if (needPermission.equals(ga.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("没有权限访问!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

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

```
 

到此java 代码就已经完成编写了. 然后抓紧时间写个```LoginController``` 吧

```
    @PostMapping("/login")
    public String login(String userName, String password,Model model) {
        HttpSession session = request.getSession();
        User user = userService.findByUserName(userName);
        if (!passwordEncoder.matches(password, user.getPassword())) {
             model.addAttribute("error", "用户名或密码错误");
            return "/pages/login";
        }
        // 这句代码会自动执行咱们自定义的 ```MyUserDetailService.java``` 类
        Authentication authentication = myAuthenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
        if (!authentication.isAuthenticated()) {
            throw new BadCredentialsException("Unknown username or password");
        }
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);
        session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
        session.setAttribute(PlatformConstant.SESSION_OPERATOR, user);
         operateLogService.saveOperateLog(user, request.getRemoteAddr());
        return "index";
    }
```
页面如下
```login.jsp```
```
<form action="/user/login"method="POST">
<table>
    <tr>
        <td>username:</td>
        <td><input type='text'name='username'></td>
    </tr>
    <tr>
        <td>password:</td>
        <td><input type='password'name='password'></td>
    </tr>
    <tr>
        <td><input name="reset"type="reset"></td>
        <td><input name="submit"type="submit"></td>
    </tr>
</table>
</form>
```
登陆之后跳转到 index.jsp 
```
这是首页,欢迎<sec:authentication property="name"/>!<br>

<sec:authentication property="authorities"/>  <br/>

<a href="admin.jsp">进入admin页面</a>

<sec:authorize url='/other1.jsp' >
    <a href="other1.jsp">权限1</a>
</sec:authorize> 

<sec:authorize url='/other2.jsp' >
<a href="other2.jsp">权限2</a>
</sec:authorize>

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

推荐阅读更多精彩内容