Shiro初体验

简介:

Apache Shiro 是 Java 的一个安全(权限)框架。
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等。

Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,而且Shiro的API也是非常简单;其基本功能点如下图所示:

[图片上传失败...(image-fb88c2-1583464531431)]

Authentication:身份认证/登录,验证用户是不是拥有相应的身份,例如账号密码登陆;

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support:Web支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

shiro自带的拦截器

默认拦截器名 拦截器类 说明(括号里的表示默认值)
认证相关:
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基于表单的拦截器;如 “/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure);
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP 身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);
logout org.apache.shiro.web.filter.authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/); 示例 “/logout=logout”
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例 “/static/**=anon”
授权相关:
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例 “/admin/**=roles[admin]”
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例 “/user/**=perms["user:create"]”
其他:
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter 不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常;

shiro认证流程

  • subject(主体)请求认证,调用subject.login(token)
  • SecurityManager (安全管理器)执行认证
  • SecurityManager通过ModularRealmAuthenticator进行认证。
  • ModularRealmAuthenticator将token传给realm,realm根据token中用户信息从数据库查询用户信息(包括身份和凭证)
  • realm如果查询不到用户给ModularRealmAuthenticator返回null,ModularRealmAuthenticator抛出异常(用户不存在)
  • realm如果查询到用户给ModularRealmAuthenticator返回AuthenticationInfo(认证信息)
  • ModularRealmAuthenticator拿着AuthenticationInfo(认证信息)去进行凭证(密码)比对。如果一致则认证通过,如果不致抛出异常(凭证错误)。

shiro授权流程

  • 对subject进行授权,调用方法isPermitted("")或者hasRole("")
  • SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
  • ModularRealmAuthorizer执行realm(自定义的CustomRealm)从数据库查询权限数据调用realm的授权方法:doGetAuthorizationInfo
  • realm从数据库查询权限数据,返回ModularRealmAuthorizer
  • ModularRealmAuthorizer调用PermissionResolver进行权限串比对
  • 如果比对后,isPermitted中"permission串"在realm查询到权限数据中,说明用户访问permission串有权限,否则没有权限,抛出异常。

实际应用

最近在项目中使用到shiro了,是用jwt做无状态登录,Redis做授权的缓存。按步骤记录一下

  • 首先应该明白的是,shiro的默认配置是通过cookie和自带的session来实现缓存和Remember Me这些功能,如果要做无状态应用,应该禁止shiro的session功能。

  • 整个项目分为账号密码登录和jwt登录两种方式,所以需要至少两种Realm,多Realm的认证策略有以下三种

    [1]AtLeastOneSuccessfulStrategy
    如果一个(或更多)验证成功,则整体的尝试被认为是成功的。如果没有一个验证成功,则整体失败。说白了就是,至少有一个Realm的验证是成功的算才认证通过,否则认证失败。

    [2]FirstSuccessfulStrategy
    第一个Realm成功验证返回的信息将被使用,其他的Realm将被忽略。如果没有一个Realm验证成功,则整体失败,和第一个的区别就在于,AtLeastOneSuccessfulStrategy是将所有的认证信息都返回,而FirstSuccessfulStrategy认定一个成功则返回。

    [3]AllSuccessfulStrategy
    所有配置的Realm都必须验证成功才算认证通过,否则认证失败。
    在这里使用AtLeastOneSuccessfulStrategy比较好。

  • 使用ModularRealmAuthenticator后,如果出现异常,shiro并没有抛出具体异常,而是捕获后重新抛出整体没有认证成功的异常,如果想要抛出具体的异常,需要继承ModularRealmAuthenticator,重写doMultiRealmAuthentication方法将异常抛出。

  • 首先应该做登录的realm,使用数据库校验就可以判断登录的用户名密码是否正确。

  • 其次是jwt登录校验,shiro并没有有关jwt的拦截器和校验器,都需要自己重写。

  • 全部配置完成后,理应对授权做缓存,使用Redis做缓存。

代码

ShiroConfig:

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, UserService userService){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("tokenFilter",createAuthFilter(userService));
        shiroFilterFactoryBean.setFilters(filterMap);

        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        map.put("/login", "noSessionCreation,anon");
        // 先测试,后面再换
        //map.put("/insuranceType/list", "noSessionCreation,tokenFilter,roles[member]");
        map.put("/**", "noSessionCreation,anon");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }


    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator(){
        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }

    @Bean
    public DefaultWebSecurityManager getSecurityManager(JwtShiroRealm jwtRealm, DbShiroRealm dbShiroRealm, CacheManager cacheManager){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealms(Arrays.asList(jwtRealm, dbShiroRealm));
        defaultWebSecurityManager.setCacheManager(cacheManager);

        //扩展父类原方法,捕获原始异常
        MultiRealmAuthenticator authenticator = new MultiRealmAuthenticator();
        //设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
        authenticator.setRealms(Arrays.asList(jwtRealm, dbShiroRealm));
        //设置多个realm认证策略,一个成功即跳过其它的
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        defaultWebSecurityManager.setAuthenticator( authenticator );

        return defaultWebSecurityManager;
    }

    @Bean("cacheManager")
    public CacheManager cacheManager(RedisTemplate redisTemplate){
        CacheManager cacheManager = new RedisCacheManager();
        ((RedisCacheManager) cacheManager).setCache(new RedisCache(redisTemplate));

        return cacheManager;
    }

    //注意不要加@Bean注解
    protected JwtAuthFilter createAuthFilter(UserService userService){
        return new JwtAuthFilter(userService);
    }

}

MultiRealmAuthenticator:

@Slf4j
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
        AuthenticationStrategy strategy = this.getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
        if (log.isTraceEnabled()) {
            log.trace("Iterating through {} realms for PAM authentication", realms.size());
        }
        Iterator var5 = realms.iterator();
        AuthenticationException authenticationException = null;
        while(var5.hasNext()) {
            Realm realm = (Realm)var5.next();
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
            if (realm.supports(token)) {
                log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
                AuthenticationInfo info = null;
                Throwable t = null;
                try {
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable var11) {
                    t = var11;
                    authenticationException = (AuthenticationException)var11;
                    if (log.isDebugEnabled()) {
                        String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
                        log.debug(msg, var11);
                    }
                }
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
                //增加此逻辑,只有authenticationException不为null,则表示有Realm较验到了异常,则立即中断后续Realm验证直接外抛
                if (authenticationException != null){
                    throw authenticationException;
                }
            } else {
                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
            }
        }

        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }
}

DbRealm:

@Component
public class DbShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;

    public DbShiroRealm() {

        this.setCredentialsMatcher((AuthenticationToken token, AuthenticationInfo info) -> {
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;
            //要验证的明文密码
            String plaintext = new String(userToken.getPassword());
            //数据库中的加密后的密文
            String hashed = info.getCredentials().toString();

            return BCrypt.checkpw(plaintext, hashed);
        });
        this.setCachingEnabled(true);
        this.setAuthorizationCachingEnabled(true);
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken userpasswordToken = (UsernamePasswordToken)token;
        String username = userpasswordToken.getUsername();

        User checkUser = new User();
        checkUser.setState(UserState.NORMAL.getState());
        // 通过Email登录
        if (Validator.isEmail(username)){
            checkUser.setEmail(username);
        }else if(Validator.isIdCard(username)){
            // 通过身份证登录
            checkUser.setIdCard(username);
        }else if(Validator.isPhone(username)){
            //通过手机号登录
            checkUser.setPhone(username);
        }else {
            // 通过用户名登录;修改用户名时必须做限定
            checkUser.setUsername(username);
        }

        User user = userService.getUserByRules(checkUser);
        if(user == null)
            throw new AuthenticationException("用户名或者密码错误");
        return new SimpleAuthenticationInfo(user, user.getPassword(),getName());
    }


    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principals.getPrimaryPrincipal();
        user.setRoles(roleService.findRolesByUserId(user.getId()));
        Set<Role> roles = user.getRoles();
        List<String> roleNames = roles.stream().map(Role::getName).collect(Collectors.toList());
        simpleAuthorizationInfo.addRoles(roleNames);

        return simpleAuthorizationInfo;
    }

}

jwtRealm:

@Component
public class JwtShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    private JwtUtil jwtUtil;

    @Autowired
    public JwtShiroRealm(JwtUtil jwtUtil){
        this.jwtUtil = jwtUtil;
        this.setCachingEnabled(true);
        this.setAuthorizationCachingEnabled(true);
        this.setCredentialsMatcher(new JwtCredentialsMatcher(jwtUtil));
    }


    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) authcToken;
        String token = jwtToken.getToken();
        Optional.ofNullable(token).orElseThrow(()-> new AuthenticationException(ResponseCode.NEED_LOGIN.getDesc()));

        Map<String, String> map;
        // 校验
        try{
            map = jwtUtil.verifyToken(token);
        }catch (TokenExpiredException e){
            throw new AuthenticationException(ResponseCode.TOKEN_EXPIRES.getDesc());
        }catch (Exception e){
            throw new AuthenticationException("token无效,请重新登陆");
        }

        String host = map.get("host");
        if (host == null || !host.equals(jwtToken.getHost())){
            throw new AuthenticationException("token地区错误,请重新登录");
        }


        int id = Integer.parseInt(map.get("id"));
        User user = userService.getUserById(id);

        if(user == null)
            throw new AuthenticationException("token过期,请重新登录");
        return new SimpleAuthenticationInfo(user, null, getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("进行数据库权限读取");

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principals.getPrimaryPrincipal();
        user.setRoles(roleService.findRolesByUserId(user.getId()));
        Set<Role> roles = user.getRoles();
        List<String> roleNames = roles.stream().map(Role::getName).collect(Collectors.toList());
        simpleAuthorizationInfo.addRoles(roleNames);

        return simpleAuthorizationInfo;
    }
}

jwtFilter:


@Slf4j
public class JwtAuthFilter extends AuthenticatingFilter {
    private static final int tokenRefreshInterval = 300;
    private String tokenError = "";
    private UserService userService;

    public JwtAuthFilter(UserService userService){
        this.userService = userService;
        this.setLoginUrl("/login");
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) //对于OPTION请求做拦截,不做token校验
            return false;

        return super.preHandle(request, response);
    }

    @Override
    protected void postHandle(ServletRequest request, ServletResponse response){
        this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response));
        request.setAttribute("jwtShiroFilter.FILTERED", true);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if(this.isLoginRequest(request, response))
            return true;
        Boolean afterFiltered = (Boolean)(request.getAttribute("jwtShiroFilter.FILTERED"));
        if( BooleanUtils.isTrue(afterFiltered))
            return true;

        boolean allowed = false;
        try {
            allowed = executeLogin(request, response);
        } catch(IllegalStateException e){ //not found any token
            e.printStackTrace();
            log.error("Not found any token");
        }catch (Exception e) {
            log.error("Error occurs when login", e);
        }
        return allowed || super.isPermissive(mappedValue);
    }


    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
        HttpServletRequest request = null;
        Optional<String> result = null;
        String host = null;
        if (servletRequest instanceof HttpServletRequest){
            request = (HttpServletRequest)servletRequest;
            String token = request.getHeader("authorization");

            result = Optional.ofNullable(token)
                    .filter(item -> item.startsWith("bearer"))
                    .map(item -> item.substring(6));

            host = WebUtil.getRealRemoteAddr(request);

        }else {
            result = Optional.empty();
        }

        return new JwtToken(result.orElse(null), host);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("application/json;charset=UTF-8");
        httpResponse.setStatus(HttpStatus.SC_LENGTH_REQUIRED);

        int errorCode = 0;
        String errorMsg = "";
        try {
            if (ResponseCode.TOKEN_EXPIRES.getDesc().equals(servletRequest.getAttribute(this.tokenError))){
                errorCode = ResponseCode.TOKEN_EXPIRES.getCode();
                errorMsg = ResponseCode.TOKEN_EXPIRES.getDesc();
            }else {
                errorCode = ResponseCode.ERROR.getCode();
                errorMsg = (String)servletRequest.getAttribute(this.tokenError);
            }
        } catch (Exception e1) {
            errorCode = ResponseCode.ERROR.getCode();
            errorMsg = ResponseCode.ERROR.getDesc();
            e1.printStackTrace();
        }

        fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
        ResponseWriter.println(httpResponse, ServerResponse.createByErrorCodeMessage(errorCode, errorMsg));
        return false;
    }


    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        request.setAttribute(this.tokenError, e.getMessage());
        log.error("Validate  fail, token:{}, error:{}", token.toString(), e.getMessage());
        return false;
    }

    protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
    }
}

JwtCredentialsMatcher:

@NoArgsConstructor
public class JwtCredentialsMatcher implements CredentialsMatcher {
    private JwtUtil jwtUtil;
    public JwtCredentialsMatcher(JwtUtil jwtUtil){
        this.jwtUtil = jwtUtil;
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
        JwtToken jwtToken = (JwtToken) authenticationToken;

        String token = jwtToken.getToken();
        // 校验
        try{
            jwtUtil.verifyToken(token);
        }catch (Exception e){
            return false;
        }

        return true;
    }
}

RedisCacheManager:

public class RedisCacheManager implements CacheManager {

    private Cache cache;

    public void setCache(Cache cache) {
        this.cache = cache;
    }

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return cache;
    }
}

RedisCache:

@Data
@NoArgsConstructor
@Slf4j
public class RedisCache<K,V> implements Cache<K,V> {

    private RedisTemplate redisTemplate;

    // 过期时间,单位为分钟
    private static final Integer expire_time = 30;
    // 缓存前缀
    private static final String cache_prefix = "shiro-cache:";

    public RedisCache(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    private String getKey(K k){
        if (k instanceof String){
            return cache_prefix + k;
        }else if(k instanceof SimplePrincipalCollection){
            return cache_prefix + ((User)((SimplePrincipalCollection)k).getPrimaryPrincipal()).getUsername();
        }

        return cache_prefix + k.toString();
    }
    @Override
    public V get(K key) throws CacheException {
        V v = (V)redisTemplate.opsForValue().get(getKey(key));
        log.info("从redis中取出权限对象[{}]" , v);
        return v;
    }

    @Override
    public V put(K k, V v) throws CacheException {
        redisTemplate.opsForValue().set(getKey(k), v, expire_time, TimeUnit.SECONDS);
        log.info("[k={},v={}]将权限存放到Redis中", k, v);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        return null;
    }

    @Override
    public void clear() throws CacheException {

    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

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

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

推荐阅读更多精彩内容