Spring Boot 集成Shiro,前后端分離權(quán)限校驗盯桦,自定義返回信息

BB兩句慈俯,Shiro的坑是真的太多了,和Spring Boot集成的時候更是多上加多

總結(jié)的拥峦,教程的文章太多了贴膘,大家有興趣自己去網(wǎng)上搜索一下吧。
本著 拎包入住略号,粘貼可用 的原則刑峡,直接上代碼洋闽。

項目源碼:https://github.com/dk980241/spring-boot-template

涉及功能:

  • Shiro使用配置
  • Shiro redis 緩存
  • Shiro session redis
  • Shiro前后端分離校驗URL
  • Shiro自定義返回信息,不做跳轉(zhuǎn)

注意點和一些個人想法都在代碼的注釋里突梦。

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匹配關(guān)系
     */
    private List<String> urlFilterList = new ArrayList<>();

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

    /**
     * 散列迭代次數(shù)
     */
    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 實現(xiàn)校驗邏輯的重寫和返回值的定義 {@link ShiroFilterFactoryBean#setFilters(java.util.Map)
     * 對一個 URL 要進行多個 Filter 的校驗诫舅。通過 {@link ShiroFilterFactoryBean#setFilterChainDefinitions(java.lang.String)} 實現(xiàn)
     * 通過 {@link ShiroFilterFactoryBean#setFilterChainDefinitionMap(java.util.Map)} 實現(xiàn)的攔截不方便實現(xiàn)實現(xiàn)多 Filter 校驗,所以這里沒有使用
     * <p>
     * 權(quán)限的名稱可以隨便指定的宫患,和 URL 配置的 Filter 有關(guān)刊懈,這里使用 {@link DefaultFilter} 默認的的權(quán)限定義,覆蓋了原權(quán)限攔截器
     * 授權(quán)Filter {@link WebUserFilter}
     * 權(quán)限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已經(jīng)實現(xiàn)緩存 {@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;
        }

        /**
         * 不要做任何處理跳轉(zhuǎn)畜吊,直接return泽疆,進行下一個filter
         *
         * @param request
         * @param response
         * @return
         * @throws Exception
         */
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            return false;
        }
    }

    /**
     * 重寫權(quán)限filter
     * <p>
     * shiro 默認 {@link org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter}
     * <p>
     * 前后端分離項目,直接獲取url進行匹配玲献,后臺配置的權(quán)限的值就是請求路徑 {@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\":\"權(quán)限不足\"}");
            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;

    /**
     * 獲取授權(quán)信息
     * <p>
     * 權(quán)限的值是前端ajax請求的路徑殉疼,角色的存在是為了方便給用戶批量賦值權(quán)限的。
     * 項目的最終實現(xiàn)是針對用戶和權(quán)限的關(guān)系捌年,不對角色作校驗
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        /**
         * 如果項目使用了 spring-boot-devtools 會導(dǎo)致類加載不同
         * 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("用戶信息查詢?yōu)榭?);
        }
        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方便后續(xù)直接使用
     *
     * @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);
    }
}

具體的登錄登出等使用方式不做贅述瓢娜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市礼预,隨后出現(xiàn)的幾起案子眠砾,更是在濱河造成了極大的恐慌,老刑警劉巖托酸,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褒颈,死亡現(xiàn)場離奇詭異,居然都是意外死亡励堡,警方通過查閱死者的電腦和手機谷丸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來应结,“玉大人刨疼,你說我怎么就攤上這事《炝洌” “怎么了揩慕?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長扮休。 經(jīng)常有香客問我迎卤,道長,這世上最難降的妖魔是什么肛炮? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任止吐,我火速辦了婚禮宝踪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘碍扔。我一直安慰自己瘩燥,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布不同。 她就那樣靜靜地躺著厉膀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪二拐。 梳的紋絲不亂的頭發(fā)上服鹅,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音百新,去河邊找鬼企软。 笑死,一個胖子當(dāng)著我的面吹牛饭望,可吹牛的內(nèi)容都是我干的仗哨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼铅辞,長吁一口氣:“原來是場噩夢啊……” “哼厌漂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起斟珊,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤苇倡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后囤踩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旨椒,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年高职,在試婚紗的時候發(fā)現(xiàn)自己被綠了钩乍。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡怔锌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出变过,到底是詐尸還是另有隱情埃元,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布媚狰,位于F島的核電站岛杀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏崭孤。R本人自食惡果不足惜类嗤,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一糊肠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧遗锣,春花似錦货裹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至笔咽,卻和暖如春搔预,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叶组。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工拯田, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人甩十。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓船庇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親枣氧。 傳聞我的和親對象是個殘疾皇子溢十,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 和往常稍微不同的是出發(fā)的時間往后了,行動更加匆忙达吞,伴著窗外的散落的雨张弛,趕到公司。 剛坐在椅子上酪劫,才恍然今天只剩自己...
    愛吃甜點的小姐姐閱讀 191評論 0 0
  • 養(yǎng) 文科 你是我匍匐朝圣的榜樣 黃土地的兒女果然不一樣 五百次深情的回望 才換來今生永久的守望 可愛的母親倚門眺望...
    文大科愛書法閱讀 182評論 0 0
  • ? 在寫Android音樂播放器 Quiet 的時候吞鸭,遇到一個奇怪的BUG, 布局的 fitSystemWin...
    summerlyy閱讀 11,430評論 0 8
  • markdown基本語法 Markdown 是一種方便記憶、書寫的純文本標(biāo)記語言覆糟,用戶可以使用這些標(biāo)記符號以最小的...
    9eb5365498d7閱讀 185評論 0 0