Spring Boot 集成Shiro,前后端分离权限校验,自定义返回信息

BB两句,Shiro的坑是真的太多了,和Spring Boot集成的时候更是多上加多

总结的,教程的文章太多了,大家有兴趣自己去网上搜索一下吧。
本着 拎包入住,粘贴可用 的原则,直接上代码。

项目源码:https://github.com/dk980241/spring-boot-template

涉及功能:

  • Shiro使用配置
  • Shiro redis 缓存
  • Shiro session redis
  • Shiro前后端分离校验URL
  • Shiro自定义返回信息,不做跳转

注意点和一些个人想法都在代码的注释里。

ShiroConfig.java

package site.yuyanjia.template.common.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import site.yuyanjia.template.website.realm.WebUserRealm;

import javax.servlet.Filter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * shiro配置
 *
 * @author seer
 * @date 2018/2/1 15:41
 */
@Configuration
@AutoConfigureAfter(RedisConfig.class)
@ConfigurationProperties(prefix = ShiroConfig.PREFIX)
@Slf4j
public class ShiroConfig {

    public static final String PREFIX = "yuyanjia.shiro";

    /**
     * Url和Filter匹配关系
     */
    private List<String> urlFilterList = new ArrayList<>();

    /**
     * 散列算法
     */
    private String hashAlgorithm = "MD5";

    /**
     * 散列迭代次数
     */
    private Integer hashIterations = 2;

    /**
     * 缓存 key 前缀
     */
    private static final String SHIRO_REDIS_CACHE_KEY_PREFIX = ShiroConfig.class.getName() + "_shiro.redis.cache_";

    /**
     * session key 前缀
     */
    private static final String SHIRO_REDIS_SESSION_KEY_PREFIX = ShiroConfig.class.getName() + "shiro.redis.session_";

    /**
     * Filter 工厂
     * <p>
     * 通过自定义 Filter 实现校验逻辑的重写和返回值的定义 {@link ShiroFilterFactoryBean#setFilters(java.util.Map)
     * 对一个 URL 要进行多个 Filter 的校验。通过 {@link ShiroFilterFactoryBean#setFilterChainDefinitions(java.lang.String)} 实现
     * 通过 {@link ShiroFilterFactoryBean#setFilterChainDefinitionMap(java.util.Map)} 实现的拦截不方便实现实现多 Filter 校验,所以这里没有使用
     * <p>
     * 权限的名称可以随便指定的,和 URL 配置的 Filter 有关,这里使用 {@link DefaultFilter} 默认的的权限定义,覆盖了原权限拦截器
     * 授权Filter {@link WebUserFilter}
     * 权限Filter {@link WebPermissionsAuthorizationFilter}
     * 登出Filter {@link WebLogoutFilter}
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put(DefaultFilter.authc.toString(), new WebUserFilter());
        filterMap.put(DefaultFilter.perms.toString(), new WebPermissionsAuthorizationFilter());
        filterMap.put(DefaultFilter.logout.toString(), new WebLogoutFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        StringBuilder stringBuilder = new StringBuilder();
        urlFilterList.forEach(s -> stringBuilder.append(s).append("\n"));
        shiroFilterFactoryBean.setFilterChainDefinitions(stringBuilder.toString());

        return shiroFilterFactoryBean;
    }

    /**
     * 安全管理器
     *
     * @param userRealm                自定义 realm {@link #userRealm(CacheManager, HashedCredentialsMatcher)}
     * @param shiroRedisSessionManager 自定义 session 管理器 {@link #shiroRedisSessionManager(RedisTemplate)}
     * @return @link org.apache.shiro.mgt.SecurityManager}
     */
    @Bean
    public SecurityManager securityManager(WebUserRealm userRealm, DefaultWebSessionManager shiroRedisSessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        securityManager.setSessionManager(shiroRedisSessionManager);
        return securityManager;
    }


    /**
     * 凭证计算匹配
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName(hashAlgorithm);
        hashedCredentialsMatcher.setHashIterations(hashIterations);
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * 用户Realm
     * <p>
     * SQL已经实现缓存 {@link site.yuyanjia.template.common.mapper.WebUserMapper}
     * shiro默认缓存这里还有点坑需要填
     *
     * @return
     */
    @Bean
    public WebUserRealm userRealm(CacheManager shiroRedisCacheManager, HashedCredentialsMatcher hashedCredentialsMatcher) {
        WebUserRealm userRealm = new WebUserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher);

        userRealm.setCachingEnabled(false);
        userRealm.setAuthenticationCachingEnabled(false);
        userRealm.setAuthorizationCachingEnabled(false);
        userRealm.setCacheManager(shiroRedisCacheManager);
        return userRealm;
    }

    /**
     * 缓存管理器
     *
     * @param redisTemplateWithJdk shiro的对象总是有这样那样的问题,所以 redisTemplate 使用 {@link org.springframework.data.redis.serializer.JdkSerializationRedisSerializer} 序列化值
     * @return
     */
    @Bean
    public CacheManager shiroRedisCacheManager(RedisTemplate redisTemplateWithJdk) {
        // TODO seer 2018/6/28 17:07 缓存这里反序列化有点问题,需要重写一下
        return new CacheManager() {
            @Override
            public <K, V> Cache<K, V> getCache(String s) throws CacheException {
                log.trace("shiro redis cache manager get cache. name={} ", s);

                return new Cache<K, V>() {
                    @Override
                    public V get(K k) throws CacheException {
                        log.trace("shiro redis cache get.{} K={}", s, k);
                        return ((V) redisTemplateWithJdk.opsForValue().get(generateCacheKey(s, k)));
                    }

                    @Override
                    public V put(K k, V v) throws CacheException {
                        log.trace("shiro redis cache put.{} K={} V={}", s, k, v);
                        V result = (V) redisTemplateWithJdk.opsForValue().get(generateCacheKey(s, k));

                        redisTemplateWithJdk.opsForValue().set(generateCacheKey(s, k), v);
                        return result;
                    }

                    @Override
                    public V remove(K k) throws CacheException {
                        log.trace("shiro redis cache remove.{} K={}", s, k);
                        V result = (V) redisTemplateWithJdk.opsForValue().get(generateCacheKey(s, k));

                        redisTemplateWithJdk.delete(generateCacheKey(s, k));
                        return result;
                    }

                    /**
                     * clear
                     * <p>
                     *     redis keys 命令会造成堵塞
                     *     redis scan 命令不会造成堵塞
                     *
                     * @throws CacheException
                     */
                    @Override
                    public void clear() throws CacheException {
                        log.trace("shiro redis cache clear.{}", s);
                        RedisConnection redisConnection = redisTemplateWithJdk.getConnectionFactory().getConnection();
                        Assert.notNull(redisConnection, "redisConnection is null");
                        try (Cursor<byte[]> cursor = redisConnection.scan(ScanOptions.scanOptions()
                                .match(generateCacheKey(s, "*"))
                                .count(Integer.MAX_VALUE)
                                .build())) {
                            while (cursor.hasNext()) {
                                redisConnection.del(cursor.next());
                            }
                        } catch (IOException e) {
                            log.error("shiro redis cache clear exception", e);
                        }
                    }

                    @Override
                    public int size() {
                        log.trace("shiro redis cache size.{}", s);
                        AtomicInteger count = new AtomicInteger(0);
                        RedisConnection redisConnection = redisTemplateWithJdk.getConnectionFactory().getConnection();
                        Assert.notNull(redisConnection, "redisConnection is null");
                        try (Cursor<byte[]> cursor = redisConnection.scan(ScanOptions.scanOptions()
                                .match(generateCacheKey(s, "*"))
                                .count(Integer.MAX_VALUE)
                                .build())) {
                            while (cursor.hasNext()) {
                                count.getAndIncrement();
                            }
                        } catch (IOException e) {
                            log.error("shiro redis cache size exception", e);
                        }
                        return count.get();
                    }

                    @Override
                    public Set<K> keys() {
                        log.trace("shiro redis cache keys.{}", s);
                        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
                        Set<K> keys = new HashSet<>();
                        RedisConnection redisConnection = redisTemplateWithJdk.getConnectionFactory().getConnection();
                        Assert.notNull(redisConnection, "redisConnection is null");
                        try (Cursor<byte[]> cursor = redisConnection.scan(ScanOptions.scanOptions()
                                .match(generateCacheKey(s, "*"))
                                .count(Integer.MAX_VALUE)
                                .build())) {
                            while (cursor.hasNext()) {
                                keys.add((K) stringRedisSerializer.deserialize(cursor.next()));
                            }
                        } catch (IOException e) {
                            log.error("shiro redis cache keys exception", e);
                        }
                        return keys;
                    }

                    @Override
                    public Collection<V> values() {
                        return null;
                    }
                };
            }
        };
    }


    /**
     * session管理器
     *
     * @param redisTemplateWithJdk shiro的对象总是有这样那样的问题,所以 redisTemplate 使用 {@link org.springframework.data.redis.serializer.JdkSerializationRedisSerializer} 序列化值
     * @return
     */
    @Bean
    public DefaultWebSessionManager shiroRedisSessionManager(RedisTemplate redisTemplateWithJdk) {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setGlobalSessionTimeout(1800000);
        defaultWebSessionManager.setSessionValidationInterval(900000);
        defaultWebSessionManager.setDeleteInvalidSessions(true);
        defaultWebSessionManager.setSessionDAO(
                new AbstractSessionDAO() {
                    @Override
                    protected Serializable doCreate(Session session) {
                        Serializable sessionId = this.generateSessionId(session);
                        log.trace("shiro redis session create. sessionId={}", sessionId);
                        this.assignSessionId(session, sessionId);
                        redisTemplateWithJdk.opsForValue().set(generateSessionKey(sessionId), session, session.getTimeout(), TimeUnit.MILLISECONDS);
                        return sessionId;
                    }

                    @Override
                    protected Session doReadSession(Serializable sessionId) {
                        log.trace("shiro redis session read. sessionId={}", sessionId);
                        return (Session) redisTemplateWithJdk.opsForValue().get(generateSessionKey(sessionId));
                    }

                    @Override
                    public void update(Session session) throws UnknownSessionException {
                        log.trace("shiro redis session update. sessionId={}", session.getId());
                        redisTemplateWithJdk.opsForValue().set(generateSessionKey(session.getId()), session, session.getTimeout(), TimeUnit.MILLISECONDS);
                    }

                    @Override
                    public void delete(Session session) {
                        log.trace("shiro redis session delete. sessionId={}", session.getId());
                        redisTemplateWithJdk.delete(generateSessionKey(session.getId()));
                    }

                    @Override
                    public Collection<Session> getActiveSessions() {
                        Set<Session> sessionSet = new HashSet<>();
                        RedisConnection redisConnection = redisTemplateWithJdk.getConnectionFactory().getConnection();
                        Assert.notNull(redisConnection, "redisConnection is null");
                        try (Cursor<byte[]> cursor = redisConnection.scan(ScanOptions.scanOptions()
                                .match(generateSessionKey("*"))
                                .count(Integer.MAX_VALUE)
                                .build())) {
                            while (cursor.hasNext()) {
                                Session session = (Session) redisTemplateWithJdk.opsForValue().get(cursor.next());
                                sessionSet.add(session);
                            }
                        } catch (IOException e) {
                            log.error("shiro redis session getActiveSessions exception", e);
                        }
                        return sessionSet;
                    }
                }
        );

        return defaultWebSessionManager;
    }

    /**
     * 生成 缓存 key
     *
     * @param name
     * @param key
     * @return
     */
    private String generateCacheKey(String name, Object key) {
        return SHIRO_REDIS_CACHE_KEY_PREFIX + name + "_" + key;
    }

    /**
     * 生成 session key
     *
     * @param key
     * @return
     */
    private String generateSessionKey(Object key) {
        return SHIRO_REDIS_SESSION_KEY_PREFIX + "_" + key;
    }


    /**
     * 重写用户filter
     * <p>
     * shiro 默认 {@link org.apache.shiro.web.filter.authc.UserFilter}
     *
     * @author seer
     * @date 2018/6/17 22:30
     */
    class WebUserFilter extends AccessControlFilter {
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
           response.setContentType("application/json");
            if (isLoginRequest(request, response)) {
                return true;
            }

            Subject subject = getSubject(request, response);
            if (subject.getPrincipal() != null) {
                return true;
            }
            response.getWriter().write("{\"response_code\":\"9000\",\"response_msg\":\"登录过期\"}");
            return false;
        }

        /**
         * 不要做任何处理跳转,直接return,进行下一个filter
         *
         * @param request
         * @param response
         * @return
         * @throws Exception
         */
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            return false;
        }
    }

    /**
     * 重写权限filter
     * <p>
     * shiro 默认 {@link org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter}
     * <p>
     * 前后端分离项目,直接获取url进行匹配,后台配置的权限的值就是请求路径 {@link WebUserRealm#doGetAuthorizationInfo(PrincipalCollection)}
     *
     * @author seer
     * @date 2018/6/17 22:41
     */
    class WebPermissionsAuthorizationFilter extends AuthorizationFilter {
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
            Subject subject = getSubject(request, response);
            HttpServletRequest httpServletRequest = ((HttpServletRequest) request);
            String url = httpServletRequest.getServletPath();
            if (subject.isPermitted(url)) {
                return true;
            }
            response.getWriter().write("{\"response_code\":\"90001\",\"response_msg\":\"权限不足\"}");
            return false;
        }

        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
            return false;
        }
    }

    /**
     * 重写登出filter
     * shiro 默认 {@link LogoutFilter}
     *
     * @author seer
     * @date 2018/6/26 2:09
     */
    class WebLogoutFilter extends LogoutFilter {
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            response.getWriter().write("{\"response_code\":\"0000\",\"response_msg\":\"SUCCES\"}");
            Subject subject = getSubject(request, response);

            if (isPostOnlyLogout()) {
                if (!WebUtils.toHttp(request).getMethod().toUpperCase(Locale.ENGLISH).equals(HttpMethod.POST.toString())) {
                    return onLogoutRequestNotAPost(request, response);
                }
            }
            try {
                subject.logout();
            } catch (SessionException ise) {
                log.trace("Encountered session exception during logout.  This can generally safely be ignored.", ise);
            }
            return false;
        }
    }

    public List<String> getUrlFilterList() {
        return urlFilterList;
    }

    public void setUrlFilterList(List<String> urlFilterList) {
        this.urlFilterList = urlFilterList;
    }

    public String getHashAlgorithm() {
        return hashAlgorithm;
    }

    public void setHashAlgorithm(String hashAlgorithm) {
        this.hashAlgorithm = hashAlgorithm;
    }

    public Integer getHashIterations() {
        return hashIterations;
    }

    public void setHashIterations(Integer hashIterations) {
        this.hashIterations = hashIterations;
    }
}

application-yml

yuyanjia:
  shiro:
    url-filter-list:
      - /website/user/user-login=anon
      - /website/user/user-logout=logout
      - /website/user/**=authc,perms
      - /**=anon

WebUserRealm

package site.yuyanjia.template.website.realm;

import org.apache.commons.collections.CollectionUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import site.yuyanjia.template.common.mapper.WebPermissionMapper;
import site.yuyanjia.template.common.mapper.WebRolePermissionMapper;
import site.yuyanjia.template.common.mapper.WebUserMapper;
import site.yuyanjia.template.common.mapper.WebUserRoleMapper;
import site.yuyanjia.template.common.model.WebPermissionDO;
import site.yuyanjia.template.common.model.WebRolePermissionDO;
import site.yuyanjia.template.common.model.WebUserDO;
import site.yuyanjia.template.common.model.WebUserRoleDO;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;


/**
 * 用户Realm
 *
 * @author seer
 * @date 2018/2/1 16:59
 */
public class WebUserRealm extends AuthorizingRealm {

    @Autowired
    private WebUserMapper webUserMapper;

    @Autowired
    private WebUserRoleMapper webUserRoleMapper;

    @Autowired
    private WebRolePermissionMapper webRolePermissionMapper;

    @Autowired
    private WebPermissionMapper webPermissionMapper;

    /**
     * 获取授权信息
     * <p>
     * 权限的值是前端ajax请求的路径,角色的存在是为了方便给用户批量赋值权限的。
     * 项目的最终实现是针对用户和权限的关系,不对角色作校验
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        /**
         * 如果项目使用了 spring-boot-devtools 会导致类加载不同
         * jar 使用 {@link sun.misc.Launcher.AppClassLoader}
         * spring-boot-devtools 使用 {@link org.springframework.boot.devtools.restart.classloader.RestartClassLoader}
         */
        Object obj = principalCollection.getPrimaryPrincipal();
        if (ObjectUtils.isEmpty(obj)) {
            throw new AccountException("用户信息查询为空");
        }
        WebUserDO webUserDO;
        if (obj.getClass().getClassLoader().equals(WebUserDO.class.getClassLoader())) {
            webUserDO = (WebUserDO) obj;
        }else{
            webUserDO = new WebUserDO();
            BeanUtils.copyProperties(obj, webUserDO);
        }

        SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();
        List<WebUserRoleDO> webUserRoleDOList = webUserRoleMapper.selectByUserId(webUserDO.getId());
        if (CollectionUtils.isEmpty(webUserRoleDOList)) {
            return authenticationInfo;
        }

        List<WebRolePermissionDO> webRolePermissionDOList = new ArrayList<>();
        webUserRoleDOList.forEach(
                webUserRoleDO -> webRolePermissionDOList.addAll(webRolePermissionMapper.selectByRoleId(webUserRoleDO.getRoleId()))
        );
        if (CollectionUtils.isEmpty(webRolePermissionDOList)) {
            return authenticationInfo;
        }

        Set<String> permissonSet = webRolePermissionDOList.stream()
                .map(webRolePermissionDO ->
                {
                    WebPermissionDO webPermissionDO = webPermissionMapper.selectByPrimaryKey(webRolePermissionDO.getPermissionId());
                    return webPermissionDO.getPermissionValue();
                })
                .collect(Collectors.toSet());
        authenticationInfo.addStringPermissions(permissonSet);
        return authenticationInfo;
    }

    /**
     * 获取验证信息
     * <p>
     * 将用户实体作为principal方便后续直接使用
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        WebUserDO webUserDO = webUserMapper.selectByUsername(username);
        if (ObjectUtils.isEmpty(webUserDO)) {
            throw new UnknownAccountException("用户 " + username + " 信息查询失败");
        }

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                webUserDO,
                webUserDO.getPassword(),
                getName()
        );
        ByteSource salt = ByteSource.Util.bytes(webUserDO.getSalt());
        authenticationInfo.setCredentialsSalt(salt);
        return authenticationInfo;
    }

    /**
     * 删除缓存
     *
     * @param principals
     */
    @Override
    protected void doClearCache(PrincipalCollection principals) {
        super.doClearCache(principals);
    }
}

具体的登录登出等使用方式不做赘述。

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

推荐阅读更多精彩内容

  • 和往常稍微不同的是出发的时间往后了,行动更加匆忙,伴着窗外的散落的雨,赶到公司。 刚坐在椅子上,才恍然今天只剩自己...
    爱吃甜点的小姐姐阅读 190评论 0 0
  • 养 文科 你是我匍匐朝圣的榜样 黄土地的儿女果然不一样 五百次深情的回望 才换来今生永久的守望 可爱的母亲倚门眺望...
    文大科爱书法阅读 180评论 0 0
  • ​ 在写Android音乐播放器 Quiet 的时候,遇到一个奇怪的BUG, 布局的 fitSystemWin...
    summerlyy阅读 11,356评论 0 8
  • markdown基本语法 Markdown 是一种方便记忆、书写的纯文本标记语言,用户可以使用这些标记符号以最小的...
    9eb5365498d7阅读 181评论 0 0