springSecurity非前后端分离配置(生产环境可用)

1 场景

1.1 说明

springSecurity作为一个权限管理系统,在生产环境使用,还是比较复杂的,涉及的相关点比较多。网上文章里,并没有比较全的配置。

本文主要将springSecurity在生产环境中使用时,需要注意到的地方,进行了相关整理。

springSecurity默认是基于session的非前后端分离场景,本文基于此场景进行配置,有时间的时候,后续会记录补充如下场景:
(1)非前后端分离
(2)基于JWT的非前后端分离
(3)网关上整合权限控制

1.2 源码

本文是基于各个应用模块单独写的配置,完整的测试代码,可关注、点赞后私信博主。所有的代码,博主均已校验过,demo可正常跑通,可直接应用到生产环境中。

1.3 版本

spring-boot版本:2.3.3.RELEASE

其他版本:

<!-- ==========【freemarker权限security标签支持】========== start -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.3</version>
    <scope>provided</scope>
</dependency>
<!-- ==========【freemarker权限security标签支持】========== end -->

2 登录

前提,屏蔽csrf防护,否则系统登录、登出无法正常访问。如需打开,需自己进行相关配置。

// 屏蔽csrf防护
http.csrf().disable();

2.1 自定义登录页面

本文前端页面,使用的freemarker

2.1.1 页面配置

springSecurity登录表单默认使用的是自己的页面,一般系统都需要进行自定义登录页面

访问系统页面时,如未认证,则自动跳转到登录页面

文件路径:resources\templates\system\main.ftlh

如下:

<form action="/doLogin" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username" value="admin"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="text" name="password" value="123456"></td>
        </tr>
        <tr>
            <td><input type="submit" value="登录"></td>
        </tr>
    </table>
</form>

如上代码所示,登录时的相关参数如下:

参数描述 参数
加载登录页面路径 /initLogin
提交登录请求路径 /doLogin
用户名 username
密码 password
2.1.2 后台代码配置
/**
  * 加载登录页面
  * @return
  */
@RequestMapping(value = {"initLogin"})
public ModelAndView initLogin(HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView("system/login");
    return modelAndView;
}

备注:提交登录请求路径(/doLogin)无需配置,此请求,会走security自己的认证流程。

2.1.3 登录请求放行

放行“登录页面”,“登录请求”等相关权限验证请求

http.authorizeRequests()
    // ......
    // 放行“登录页面”,“登录请求”,“退出”等相关权限验证请求
    .antMatchers("/initLogin", "/doLogin", "/doLogout").permitAll()
    // 任意请求需认证通过
    .anyRequest().authenticated();
2.1.4 登录参数配置

security的登录请求路径和参数都是默认配置的,这里我们更改为自己的请求配置:

http.formLogin()
    // 登录时自定义“用户”参数名(默认为:username)
    .usernameParameter("username")
    // 登录时自定义“用户”参数名(默认为:password)
    .passwordParameter("password")
    // 自定义登录页面(默认为:login/GET)
    .loginPage("/initLogin")
    // 自定义登录请求路径(默认为:login/POST)
    .loginProcessingUrl("/doLogin")

2.2 登录校验逻辑

2.1 自定义登录用户对象

扩展security自带的用户对象(org.springframework.security.core.userdetails.User),扩展自定义属性

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;

/**
 * 自定义用户对象
 */
public class LoginUser extends User {
    
    /**
     * 自定义用户属性
     */
    private String departmentCode;
    
    public LoginUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }
    
    public String getDepartmentCode() {
        return departmentCode;
    }
    
    public void setDepartmentCode(String departmentCode) {
        this.departmentCode = departmentCode;
    }
}
2.2 自定义用户认证service

自定义用户认证,主要是将用户根据用户名,从数据库中查询出来。组装好org.springframework.security.core.userdetails.UserDetails的实现类对象,交由security进行验证。后续security的验证,如未抛出异常,则认证通过,否则认证失败,然后,可根据抛出的异常类型,来识别认证失败的原因。

/**
 * 自定义用户认证service
 */
@Component
public class CustomUserDetailsService implements UserDetailsService {
    
    @Resource
    private UserService userService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // ========== 【1】校验参数 ========== 
        if (StringUtils.isBlank(username)) {
            throw new UsernameNotFoundException("用户代码为空");
        }
        
        // ========== 【2】查询用户信息 ========== 
        UserInfo userInfo = userService.getUserInfoByUserName(username);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        // 密码
        String password = userInfo.getPassword();
        // 用户其他信息(用户部门代码)
        String departmentCode = userInfo.getDepartmentCode();
        
        // ========== 【3】查询授权信息 ========== 
        // 角色代码和授权代码,均在此列表中配置(角色代码前加“ROLE_”,来和权限区分)
        List<String> authorityList = new ArrayList<>();
        // 初始化角色信息
        List<String> roleCodeList = userService.getRoleCodeListByUserName(username);
        if (CollectionUtils.isNotEmpty(roleCodeList)) {
            for (String roleCode : roleCodeList) {
                authorityList.add("ROLE_" + roleCode);
            }
        }
        // 初始化权限信息
        List<String> permissionCodeList = userService.getPermissionCodeListByUserName(username);
        if (CollectionUtils.isNotEmpty(permissionCodeList)) {
            authorityList.addAll(permissionCodeList);
        }
        
        // ========== 【4】组装用户信息 ==========
        // 组装通用信息
        LoginUser loginUser = new LoginUser(username, password, AuthorityUtils.createAuthorityList(authorityList.toArray(new String[0])));
        // 组装自定义用户信息
        loginUser.setDepartmentCode(departmentCode);
        
        return loginUser;
    }
}

2.3 认证通过逻辑

认证通过后,需进行页面跳转,有两种方式,一种是跳转到访问登录页面前的页面(访问某个页面,因为未认证,自动跳转到登录页面,当登录成功后,自动跳转到此页面,而不是系统主页面),一种是跳转到系统主页面。

可根据实际业务需求选择,这里选择第一种。

一般登录成功后,系统会执行自定义逻辑,如记录登录IP、登录时间等,这里使用自定义登录成功逻辑

2.3.1 自定义登录成功处理器
/**
 * 自定义登录成功处理器
 */
@Component
public class CustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        // TODO 自定义登录成功逻辑......
        System.out.println("自定义登录成功逻辑......");
        super.onAuthenticationSuccess(request, response, authentication);
    }
}
2.3.2 配置登录成功逻辑
  • 注入bean
/**
  * 自定义登录成功逻辑
 */
@Autowired
private CustomLoginSuccessHandler customLoginSuccessHandler;
  • 配置执行器
http.formLogin()
    // 自定义登录成功forward路径
    //.successForwardUrl("/")
    // 自定义登录成功redirect路径【默认】(登录成功后,页面重定向到跳转登录页面前的页面Referer)
    //.defaultSuccessUrl("/")
    // 自定义登录成功逻辑(redirect主页面+自定义业务逻辑)
    .successHandler(customLoginSuccessHandler)
    // 自定义登录成功redirect路径(登录成功后,页面重定向到设置的登录成功页面:"/")
    //.defaultSuccessUrl("/", true)
2.3.3 登录主页面后台代码
/**
  * 系统主页面
  * @return
  */
@RequestMapping(value = {"/"})
public ModelAndView main() {
    ModelAndView modelAndView = new ModelAndView("system/main");

    // 获取当前登录人信息
    LoginUser loginUser = SecurityUtils.getLoginUser();
    modelAndView.addObject("loginUser", loginUser);

    return modelAndView;
}

2.4 获取认证信息

security认证通过后,会将认证信息,保存在ThreadLocal中,故可以通过其自带的静态方法获取:

SecurityContextHolder.getContext().getAuthentication()

此处可获取2.2中封装的自定义对象:LoginUser。

/**
 * security工具类
 */
public class SecurityUtils {
    
    /**
     * 默认角色前缀
     */
    private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
    
    /**
     * 获取认证信息
     * @return
     */
    public static Authentication getAuthentication() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        if (securityContext != null) {
            return securityContext.getAuthentication();
        }
        return null;
    }
    
    /**
     * 获取当前登录用户对象
     * @return
     */
    public static LoginUser getLoginUser() {
        Authentication authentication = SecurityUtils.getAuthentication();
        if (authentication != null) {
            return (LoginUser) authentication.getPrincipal();
        }
        return null;
    }
}

2.5 认证失败逻辑

认证失败时,跳转到登录页面,并返回错误信息。

这里我们通过forward到认证失败页面(即登录页面),由于使用的是forward进行的跳转,故可以获取request中的属性WebAttributes.AUTHENTICATION_EXCEPTION,来获取异常信息,来返回到前台。

也可通过redirect到认证失败页面(登录页面),但是请求中无法获取失败异常信息WebAttributes.AUTHENTICATION_EXCEPTION。
可以考虑自定义认证失败逻辑failureHandler,来实现此功能。

2.5.1 认证失败后台
/**
  * 登录失败页面
  * @param request
  * @return
  */
@RequestMapping(value = {"loginFail"})
public ModelAndView loginFail(HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView("system/login");
    String error = null;
    // 登录异常处理
    Object exception = request.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    if (exception instanceof AuthenticationException) {
        if(exception instanceof UsernameNotFoundException){
            // 自己抛出异常信息
            error = ((UsernameNotFoundException) exception).getMessage();
        }else if (exception instanceof BadCredentialsException){
            error = "用户名或者密码输入错误,请重新输入!";
        }else if (exception instanceof LockedException){
            error = "账户被锁定,请联系管理员!";
        }else if (exception instanceof CredentialsExpiredException){
            error = "密码过期,请联系管理员!";
        }else if (exception instanceof AccountExpiredException){
            error = "账户过期,请联系管理员!";
        }else if (exception instanceof DisabledException){
            error = "账户被禁用,请联系管理员!";
        }else{
            error = "认证失败";
        }
    }
    modelAndView.addObject("error", error);
    return modelAndView;
}
2.5.2 认证失败配置
http.formLogin()
    // 自定义失败forward路径(默认为:loginPage + "?error")
    .failureForwardUrl("/loginFail")

2 登出

2.1 页面配置

<a href="/doLogout">退出</a>

2.2 后台代码

登出,走的是security的逻辑,无需自己写后台代码。

2.3 登出请求放行

有可能执行登出操作的时候,session已失效,因此登出系统也需要放行请求,不进行认证校验

http.authorizeRequests()
    // ......
    // 放行“登录页面”,“登录请求”,“退出”等相关权限验证请求
    .antMatchers("/initLogin", "/doLogin", "/doLogout").permitAll()
    // 任意请求需认证通过
    .anyRequest().authenticated();

2.4 自定义登出处理器

一般系统登出时,也会进行相关业务操作,如记录日志,发送消息等。

2.4.1 自定义登出成功处理器
/**
 * 自定义登出成功处理器
 **/
@Configuration
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // TODO 自定义登出成功逻辑......
        System.out.println("自定义登出成功逻辑......");
        super.onLogoutSuccess(request, response, authentication);
    }
}
2.4.2 配置登录成功逻辑
  • 注入bean
/**
  * 自定义登出成功逻辑
  */
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
  • 配置执行器
// ---------- [登出系统] ----------
http.logout()
    // 删除认证信息(默认为true)
    .clearAuthentication(true)
    // 退出系统时使session无效(默认为true)
    .invalidateHttpSession(true)
    // 退出系统时,自定义请求路径
    .logoutUrl("/doLogout")
    // 自定义登出成功逻辑(redirect登录页面+自定义业务逻辑)
    .logoutSuccessHandler(customLogoutSuccessHandler);

3 URL授权验证

security可以对指定URL进行授权验证判断。个人认为主要是用来对某些特殊访问请求进行专门的认证(如api),一般不会在此配置角色权限的验证

3.1 验证配置
// ---------- [基于表单的身份验证] ----------
// 顺序很重要,从上而下依次验证
http.authorizeRequests()
    // ---------- 自定义基于URL授权验证[也可加http方式限制:(HttpMethod method, String... antPatterns)]
    // 判断是否有某权限
    .antMatchers("/noPower/**").hasAuthority("noPower")
    // 判断是否有某角色
    .antMatchers("/admin/**").hasRole("admin")
    // 判断是否有任一权限
    .antMatchers("/user/list").hasAnyAuthority("user:list", "user:all")
    // 自定义权限校验(参数来自WebSecurityExpressionRoot属性)
    .antMatchers("/api/**").access("@customAccessForApi.hasPermission(request,authentication)")
    // 放行“登录”,“退出”等相关权限验证请求
    .antMatchers("/initLogin", "/doLogin", "/doLogout").permitAll()
    // 任意请求需认证通过
    .anyRequest().authenticated();

认证通过对请求进行拦截,如antMatchers,可以使用多种认证方式:

  • 使用hasRole对角色进行认证

  • 可以使用hasAuthority对权限认证。

  • 通过自定义逻辑对请求进行进行认证

3.2 自定义逻辑验证

3.2.1 自定义验证逻辑
/**
 * 自定义权限监测
 */
@Component
public class CustomAccessForApi {
    /**
     * 权限监测
     */
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        if (StringUtils.startsWith(request.getRemoteAddr(), "192.168.56")) {
            // 满足条件的IP,可以访问此接口
            return true;
        }
        return false;
    }
}
3.2.2 配置自定义验证逻辑
 http.authorizeRequests()
     // 自定义权限校验(参数来自WebSecurityExpressionRoot属性)
     .antMatchers("/api/**").access("@customAccessForApi.hasPermission(request,authentication)")

4 方法授权验证

security可以对具体某个方法进行授权验证判断,一般加在Controller的对外请求方法上。

此方法,需要配置注解@EnableGlobalMethodSecurity开启方法前后权限判断,一般使用@PreAuthorize进行方法执行前判断

// 开启全局“方法安全”控制(开启方法前后权限判断,一般使用@PreAuthorize进行方法执行前判断)
@EnableGlobalMethodSecurity(prePostEnabled = true)

4.1 判断是否有角色

@PreAuthorize("hasRole('admin')")
@ResponseBody
@RequestMapping("add")
public String add() {
    return "department add ...";
}

4.2 判断是否有权限

@PreAuthorize("hasAuthority('department:list')")
@RequestMapping("list")
public ModelAndView list() {
    return new ModelAndView("department/department_list");
}

4.3 自定义权限判断

4.3.1 自定义校验器
@Configuration
public class CustomAccessForDepartmentDelete {
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        return false;
    }
}
4.3.2 配置自定义校验器
// 自定义权限控制,参数前需加#
@PreAuthorize("@customAccessForDepartmentDelete.hasPermission(#request,#authentication)")
@ResponseBody
@RequestMapping("delete")
public String delete() {
    return "department delete ...";
}

5 前端页面授权验证

security也可以在前端页面,加标签,来控制页面元素的展示。这里前端使用的是freemarker,需要额外做些配置,才可以使用。

5.1 freemarker配置security标签

5.1.1 maven依赖
<!-- ==========【freemarker权限security标签支持】========== start -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.3</version>
    <scope>provided</scope>
</dependency>
<!-- ==========【freemarker权限security标签支持】========== end -->
5.1.2 拷贝tld文件

spring-security-taglibs/META-INF/security.tld,拷贝到拷贝到resource的目录tags中。

5.1.3 配置
/**
 * Freemarker的Security标签支持
 */
@Configuration
public class FreemarkerSecurityTaglibConfig{
    
    /**
     * security标签路径(来自"spring-security-taglibs/META-INF/security.tld")<br>
     * 此文件需拷贝到resource的目录tags中
     */
    private static final String SECURITY_TLD_PATH="/tags/security.tld";
    
    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;
    
    @PostConstruct
    public void freeMarkerConfigurer() {
        List<String> classpathTlds = new ArrayList<>();
        classpathTlds.add(SECURITY_TLD_PATH);
        freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(classpathTlds);
    }
}

5.2 引入标签

freemarker前台文件ftlh,文件头,引入标签:

<#assign security=JspTaglibs["http://www.springframework.org/security/tags"] />

5.3 使用

5.3.1 判断有无角色
<@security.authorize access="hasRole('admin')">
    <a href="/department/test">test按钮</a>
</@security.authorize>
5.3.2 判断有无权限
<@security.authorize access="hasAuthority('department:test')">
    <a href="/department/test">test按钮</a>
</@security.authorize>

6 api验证授权

有时候,我们希望在java代码中直接判断有无某角色、有无某权限。作者对此进行了代码封装,可通过静态方法进行判断。

此判断,暂不支持权限继承。

6.1 封装

/**
 * security工具类
 */
public class SecurityUtils {
    
    /**
     * 默认角色前缀
     */
    private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
    
    /**
     * 获取认证信息
     * @return
     */
    public static Authentication getAuthentication() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        if (securityContext != null) {
            return securityContext.getAuthentication();
        }
        return null;
    }
    
    /**
     * 判断有某权限(暂不支持权限继承)
     * @return
     */
    public static boolean hasAuthority(String authority) {
        if (StringUtils.isNotEmpty(authority)) {
            return SecurityUtils.hasAnyAuthorityName(null, authority);
        }
        return false;
    }
    
    /**
     * 判断有任一权限(暂不支持权限继承)
     * @param authorityArr
     * @return
     */
    public static boolean hasAnyAuthority(String... authorityArr) {
        if (authorityArr != null && authorityArr.length > 0) {
            return SecurityUtils.hasAnyAuthorityName(null, authorityArr);
        }
        return false;
    }
    
    /**
     * 判断有某角色(暂不支持权限继承)
     * @param role
     * @return
     */
    public static boolean hasRole(String role) {
        if (StringUtils.isNotEmpty(role)) {
            return SecurityUtils.hasAnyAuthorityName(DEFAULT_ROLE_PREFIX, role);
        }
        return false;
    }
    
    /**
     * 判断有任一角色(暂不支持权限继承)
     * @param roleArr
     * @return
     */
    public static boolean hasAnyRole(String... roleArr) {
        if (roleArr != null && roleArr.length > 0) {
            return SecurityUtils.hasAnyAuthorityName(DEFAULT_ROLE_PREFIX, roleArr);
        }
        return false;
    }
    
    /**
     * 判断是否满足通用权限信息,有一个满足即为满足(包括角色和权限)
     * @param prefix         前缀
     * @param authorityNames 权限名称
     * @return
     */
    private static boolean hasAnyAuthorityName(String prefix, String... authorityNames) {
        if (authorityNames != null && authorityNames.length > 0) {
            Set<String> authoritySet = SecurityUtils.getAuthoritySet();
            if (CollectionUtils.isNotEmpty(authoritySet)) {
                for (String authorityName : authorityNames) {
                    String defaultedRole = getRoleWithDefaultPrefix(prefix, authorityName);
                    if (authoritySet.contains(defaultedRole)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    /**
     * 获取当前用户权限集合信息
     * @return
     */
    private static Set<String> getAuthoritySet() {
        Authentication authentication = SecurityUtils.getAuthentication();
        if (authentication != null) {
            Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();
            return AuthorityUtils.authorityListToSet(userAuthorities);
        }
        return null;
    }
    
    /**
     * 如果defaultRolePrefix为非空且role的开头不是defaultRolePrefix,则使用defaultRolePrefix前缀角色。
     * @param defaultRolePrefix
     * @param role
     * @return
     */
    private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
        if (role == null) {
            return role;
        }
        if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
            return role;
        }
        if (role.startsWith(defaultRolePrefix)) {
            return role;
        }
        return defaultRolePrefix + role;
    }
}

6.2 使用

java代码中直接使用静态方法判断即可,如下:

// 判断有角色
SecurityUtils.hasRole("admin");
// 判断有任一角色
SecurityUtils.hasAnyRole("admin","user");
// 判断有权限
SecurityUtils.hasAuthority("user:add");
// 判断有任一权限
SecurityUtils.hasAnyAuthority("user:add","user:edit");

7 自定义授权验证失败返回方式

当系统访问认知失败时,默认返回授权验证的错误页面,这种方式对于ajax的请求,十分不友好。

因此我们希望在验证授权失败时,如果是json请求,则返回json格式的错误信息,如果是其他请求,则返回错误页面。

7.1 前台页面

授权验证失败页面accessDenied.ftlh

<body>
无访问权限
</body>

7.2 后台代码

/**
  * 无访问权限页面
  * @return
  */
@RequestMapping("accessDenied")
public ModelAndView accessDenied() {
    return new ModelAndView("system/accessDenied");
}

7.3 配置

// ---------- [异常处理]ExceptionTranslationFilter ----------
http.exceptionHandling()
    // 认证失败(不使用默认的表单form登录认证时,可使用此方式)
    //.authenticationEntryPoint((request, response, authException) -> {
    //})
    // 访问拒绝句柄(认证通过后,无操作权限时)
    .accessDeniedHandler((request, response, accessDeniedException) -> {
        String contentType = request.getHeader("content-type");
        boolean jsonRequestFlag = (contentType != null && contentType.contains("json"));
        // 判断是否是json请求
        if (jsonRequestFlag) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.setContentType("application/json;charset=UTF-8");
            Writer writer = response.getWriter();
            JSONObject json = new JSONObject();
            json.put("success", false);
            json.put("message", "无操作权限:" + accessDeniedException.getMessage());
            writer.write(json.toJSONString());
            writer.flush();
            writer.close();
        } else {
            request.getRequestDispatcher("/accessDenied").forward(request, response);
        }
    });

8 分布式session共享

可使用spring-session或tomcat-redis-session-manager。后续补充

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

推荐阅读更多精彩内容