SpringBoot+Shiro+Vue前后端分離項(xiàng)目通過JWT實(shí)現(xiàn)自動(dòng)登錄

雖然 Shiro 本身可以支持?jǐn)U展 RememberMe 功能,但僅限于傳統(tǒng)項(xiàng)目
因?yàn)?Shiro 的用戶信息是基于 Session 進(jìn)行管理薄榛,在前后端分離的項(xiàng)目中無法實(shí)現(xiàn) Session 狀態(tài)的前后統(tǒng)一
所以本文通過 JWT 對(duì) Shiro 原生的 Session 控制進(jìn)行替換迫像,從而實(shí)現(xiàn)用戶信息的前后傳遞及判斷

更多精彩

涉及資料

  1. 一個(gè)已經(jīng)實(shí)現(xiàn)的例子
  2. JWT官網(wǎng)
  3. JWT源碼

導(dǎo)入項(xiàng)目所需的依賴

  1. 對(duì)于 SpringBoot 和 Shiro 的依賴此處不重復(fù)介紹
  2. 以下是 JWT 的依賴包领迈,就一個(gè)即可
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.2.0</version>
</dependency>

創(chuàng)建 JWTToken 替換 Shiro 原生 Token

  1. Shiro 原生的 Token 中存在用戶名和密碼以及其他信息 [驗(yàn)證碼形耗,記住我]
  2. 在 JWT 的 Token 中因?yàn)?strong>已將用戶名和密碼通過加密處理整合到一個(gè)加密串中哥桥,所以只需要一個(gè) token 字段即可
public class JWTToken implements AuthenticationToken {

    // 密鑰
    private String token;

    public JWTToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

創(chuàng)建 JWTUtil 整合 JWT 相關(guān)操作

  1. 在這個(gè)工具類中可以實(shí)現(xiàn)對(duì)用戶名和密碼的加密處理校驗(yàn) token 是否正確激涤,獲取用戶名等操作
  2. Algorithm algorithm = Algorithm.HMAC256(password) 是對(duì)密碼進(jìn)行加密后再與用戶名混淆在一起
  3. 在簽名時(shí)可以通過 .withExpiresAt(date) 指定 token 的過期時(shí)間
public class JWTUtil {

    // 過期時(shí)間30天
    private static final long EXPIRE_TIME = 24 * 60 * 30 * 1000;

    /**
     * 校驗(yàn)token是否正確
     *
     * @param token    密鑰
     * @param username 登錄名
     * @param password 密碼
     * @return
     */
    public static boolean verify(String token, String username, String password) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(password);

            JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();

            DecodedJWT jwt = verifier.verify(token);

            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 獲取登錄名
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);

            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成簽名
     *
     * @param username
     * @param password
     * @return
     */
    public static String sign(String username, String password) {
        try {
            // 指定過期時(shí)間
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);

            Algorithm algorithm = Algorithm.HMAC256(password);

            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

}

創(chuàng)建 JWTFilter 實(shí)現(xiàn)前端請(qǐng)求統(tǒng)一攔截及處理

  1. executeLogin() 方法中的 getSubject(request, response).login(token) 就是觸發(fā) Shiro Realm 自身的登錄控制拟糕,具體內(nèi)容需要手動(dòng)實(shí)現(xiàn)
  2. executeLogin() 始終返回 true 的原因是因?yàn)榫唧w的是否登錄成功的判斷,需要在 Realm 中手動(dòng)實(shí)現(xiàn)倦踢,此處不做統(tǒng)一判斷
  3. LOGIN_SIGN 是前端放置在 headers 頭文件中的登錄標(biāo)識(shí)送滞,如果用戶發(fā)起的請(qǐng)求是需要登錄才能正常返回的,那么頭文件中就必須存在該標(biāo)識(shí)并攜帶有效值
public class JWTFilter extends BasicHttpAuthenticationFilter {

    // 登錄標(biāo)識(shí)
    private static String LOGIN_SIGN = "Authorization";

    /**
     * 檢測(cè)用戶是否登錄
     * 檢測(cè)header里面是否包含Authorization字段即可
     *
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;

        String authorization = req.getHeader(LOGIN_SIGN);

        return authorization != null;

    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        String header = req.getHeader(LOGIN_SIGN);

        JWTToken token = new JWTToken(header);

        getSubject(request, response).login(token);

        return true;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                throw new TSharkException("登錄權(quán)限不足辱挥!", e);
            }
        }

        return true;
    }

    /**
     * 對(duì)跨域提供支持
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請(qǐng)求累澡,這里我們給option請(qǐng)求直接返回正常狀態(tài)
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

自定義 ShiroDatabaseRealm 實(shí)現(xiàn) Shiro Realm 的登錄控制

  1. 重寫 Realmsupports() 方法是通過 JWT 進(jìn)行登錄判斷的關(guān)鍵
    • 因?yàn)榍拔闹袆?chuàng)建了 JWTToken 用于替換 Shiro 原生 token
    • 所以必須在此方法中顯式的進(jìn)行替換,否則在進(jìn)行判斷時(shí)會(huì)一直失敗
  2. 登錄的合法驗(yàn)證通常包括 token 是否有效 般贼、用戶名是否存在密碼是否正確
    • 通過 JWTUtil 對(duì)前端傳入的 token 進(jìn)行處理獲取到用戶名
    • 然后使用用戶名前往數(shù)據(jù)庫獲取到用戶密碼
    • 再將用戶面?zhèn)魅?JWTUtil 進(jìn)行驗(yàn)證即可
public class ShiroDatabaseRealm extends AuthorizingRealm {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ShiroAuthService shiroAuthService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("doGetAuthorizationInfo+" + principals.toString());

        String username = JWTUtil.getUsername(principals.toString());
        MemberDTO member = shiroAuthService.getPrincipal(username);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        List<String> userPermissions = shiroAuthService.getPermissions(member.getId());

        // 基于Permission的權(quán)限信息
        info.addStringPermissions(userPermissions);

        return info;
    }

    /**
     * 使用JWT替代原生Token
     *
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
        String token = (String) authcToken.getCredentials();

        String username = JWTUtil.getUsername(token);

        MemberDTO member = shiroAuthService.getPrincipal(username);

        // 用戶不會(huì)空
        if (member != null) {
            // 判斷是否禁用
            if (member.getStatus().equals(MemberStatus.disableStatus)) {
                throw new DisabledAccountException("901");
            }

            // 密碼驗(yàn)證
            if (!JWTUtil.verify(token, username, member.getLoginPassword())) {
                throw new UnknownAccountException("900");
            }

            return new SimpleAuthenticationInfo(token, token, "realm");
        } else {
            throw new UnknownAccountException("900");
        }
    }

}

在 ShiroConfiguration 中將所有的請(qǐng)求指向 JWT

  1. 指定手工實(shí)現(xiàn)的 ShiroDatabaseRealm 用于傳入 DefaultWebSecurityManager
  2. securityManager 中關(guān)閉默認(rèn)的 Session 控制
    • 因?yàn)樵谇昂蠓蛛x項(xiàng)目中前端是無法獲取到后端 Session 的奥吩,即無法實(shí)現(xiàn)用戶登錄狀態(tài)的同步
  3. shiroFilterFactoryBean() 中傳入自定義的 TokenFilter
    • 并將所有的請(qǐng)求指向該過濾器 filterRuleMap.put("/**", "jwt")
@Configuration
@ConditionalOnWebApplication
public class ShiroConfiguration {

    @Bean
    public ShiroDatabaseRealm shiroDatabaseRealm() {
        return new ShiroDatabaseRealm();
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(ShiroDatabaseRealm shiroDatabaseRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroDatabaseRealm);

        // 關(guān)閉自帶session
        DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
        evaluator.setSessionStorageEnabled(false);

        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        subjectDAO.setSessionStorageEvaluator(evaluator);

        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());

        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);

        Map<String, String> filterRuleMap = new HashMap<>();

        filterRuleMap.put("/**", "jwt");

        factoryBean.setFilterChainDefinitionMap(filterRuleMap);

        return factoryBean;
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

執(zhí)行登錄操作時(shí)對(duì)用戶信息進(jìn)行基礎(chǔ)驗(yàn)證并簽名返回

  1. ResponseData 是前后分離項(xiàng)目中用于統(tǒng)一后端返回?cái)?shù)據(jù)的類哼蛆,必不可少
    • 一般包含狀態(tài)值和返回的具體內(nèi)容
@RestController
@RequestMapping("")
@Api("用戶登錄")
public class LoginController extends AbstractBaseController {

    @Autowired
    private LoginServiceImpl loginService;

    @PostMapping("/login")
    @ApiOperation("登錄")
    public ResponseData login(@RequestParam String username, @RequestParam String password) {
        return new SimpleActionHandler(request) {
            @Override
            public void doAction(ResponseData responseData) throws Exception {
                responseData.setData(loginService.jwtLogin(username, password));
            }
        }.handle();
    }

}
  1. 進(jìn)行登錄操作處理時(shí)需要判斷數(shù)據(jù)有效性,即 數(shù)據(jù)是否為空霞赫,密碼是否正確
  2. 一般為了用戶隱私和安全起見腮介,數(shù)據(jù)庫中存入的密碼都是經(jīng)過 不可逆混淆 處理的
    • 所以此處在通過 JWTUtil 簽名之前,需要將用戶傳入的密碼進(jìn)行相同混淆端衰,再將混淆后的兩個(gè)密碼進(jìn)行對(duì)比
    • 如果密碼正確叠洗,通過相同規(guī)則混淆后的密碼也會(huì)相同
@Service
public class LoginServiceImpl extends AbstractBaseService {

    @Autowired
    private MemberServiceImpl memberService;

    /**
     * 用戶登錄
     *
     * @param username
     * @param password
     * @return
     */
    public String jwtLogin(String username, String password) {
        Assert.notNull(username, "用戶名不能為空");
        Assert.notNull(password, "密碼不能為空");

        // 獲取用戶密碼混淆值
        MemberDTO member = memberService.getMemberSalt(username);

        // 加密當(dāng)前用戶輸入密碼
        byte[] bytePassword = DigestUtil.sha1(password.getBytes(), EncodeUtils.hexDecode(member.getSalt()), Constants.PASSWORD_HASH_INTERATIONS);
        String encodePassword = EncodeUtils.hexEncode(bytePassword);

        if (!encodePassword.equals(member.getLoginPassword())) {
            throw new TSharkException("900");
        }

        return JWTUtil.sign(username, encodePassword);
    }
}

在前端的請(qǐng)求發(fā)起基類中對(duì)頭文件進(jìn)行統(tǒng)一傳遞

  1. 由于本項(xiàng)目用的 SPA 模式,除了歡迎頁不需要登錄旅东,其余頁面都需要登錄后才能訪問
    • 因此在基類中對(duì)頭文件進(jìn)行統(tǒng)一處理灭抑,默認(rèn)所有請(qǐng)求都會(huì)傳遞用戶登錄狀態(tài)
    • 個(gè)別不需要傳遞登錄狀態(tài)的,再進(jìn)行單獨(dú)處理
  2. 在頭文件中指定 Authorization 自定義信息抵代,對(duì)應(yīng)的 token 值是用戶登錄后存入到 vuex 中的數(shù)組
import axios from 'axios'
import router from 'router'
import store from 'store'
import qs from 'qs'
import { SET_USER_INFO } from 'store/mutation-types'
import * as config from 'assets/scripts/config/config'

axios.defaults.withCredentials = true

const setUserInfo = function (user) {
  store.commit(SET_USER_INFO, user)
}

const getUserInfo = function () {
  return store.state.userInfo.token
}

export default function fetch(options) {
  return new Promise((resolve, reject) => {
    const instance = axios.create({
      baseURL: `${config.serverBaseUrl}mop`,
      timeout: 10000,
      withCredentials: true,
      credentials: 'include'
    })

    // 針對(duì)頭文件是否為空腾节,會(huì)做一些權(quán)限控制
    if (options.headers === undefined) {
      // 沒有指定頭文件的使用默認(rèn)頭文件,需要傳入用戶信息
      options.headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': getUserInfo()
      }
    }

    // 表單提交格式需要進(jìn)行參數(shù)轉(zhuǎn)換
    if (options.headers['Content-Type'].indexOf('x-www-form') !== -1) {
      // 參數(shù)格式轉(zhuǎn)換
      options.data = qs.stringify(options.data)
    }

    instance(options).then((response) => {
      let result = response.data

      // 無數(shù)據(jù)
      if (!result) {
        return false
      }

      // 未授權(quán)
      if (result.status === config.UNAUTHORIZED_CODE || result.data === config.UNAUTHORIZED_CODE) {
        // 清空用戶信息
        setUserInfo(null)
        // 跳轉(zhuǎn)至登錄界面
        router.replace({name: 'portal'})
        return false
      }

      resolve(result)
      return false
    }).catch((error) => {
      reject(error)

      // 清空用戶信息
      setUserInfo(null)

      router.replace({name: 'portal'})
    })
  })
}

單獨(dú)處理不需要頭文件的請(qǐng)求

  1. 由于 fetch.js 中對(duì)頭文件存在 undefined 的判斷荤牍,所以外部如果傳入頭文件案腺,則可以替換默認(rèn)的頭文件配置
import fetch from 'assets/scripts/fetch/fetch'
import * as config from 'assets/scripts/config/config'

let baseUrl = '/api/portals'

/**
 * 輪播列表
 */
export function covers() {
  const url = `${baseUrl}/covers`

  return fetch({
    url: url,
    method: config.GET,
    headers: config.VISITOR_HEADER
  })
}
  1. 實(shí)際開發(fā)中通常會(huì)存在一個(gè) config.js 文件,用于存儲(chǔ)一系列通用配置信息
// 游客頭文件
export const VISITOR_HEADER = {
  'Content-Type': 'application/x-www-form-urlencoded'
}

用戶登錄成功后將后端返回的 token 值存入 cookie

  1. 登錄操作實(shí)際應(yīng)該對(duì)用戶名和密碼進(jìn)行有效性判斷康吵,以及登錄失敗后的錯(cuò)誤提示劈榨,此處都省略,重點(diǎn)突出將 token 值存入 vuex 的步驟
<template>
  <div ref="portal" class="container portal-panel">
      ...
    <mt-field placeholder="請(qǐng)輸入手機(jī)號(hào)或郵箱" v-model.trim="user.username" :state="state.username" 
                        @keyup.enter.native="sendLogin"></mt-field>
    <mt-field placeholder="請(qǐng)輸入密碼" type="password" v-model.trim="user.password" :state="state.password"
                        @keyup.enter.native="sendLogin"></mt-field>
    <mt-button type="primary" @click.native="sendCaptcha">{{captchaText}}</mt-button>
      ...
  </div>
</template>

<script type="text/ecmascript-6">
  import { mapMutations } from 'vuex'
  import { SET_USER_INFO } from 'store/mutation-types'

  export default {
    name: 'portal',
    data() {
      return {
        countDown: 0,
        timer: null,
        user: {
          username: '',
          password: ''
        }
      }
    },
    methods: {
      ...mapMutations({
        set_user_info: SET_USER_INFO
      }),
      // 用戶登錄
      sendLogin() {
        ...
        this.$fetch.login.login({
          username: this.user.username,
          password: this.user.password
        }).then((res) => {
            ...
          this.$toast.success('登錄成功')

          this.set_user_info({
            token: res.data,
            isLogin: true
          })
            ...
        })
      }
        ...
  }
</script>

<style lang="stylus" rel="stylesheet/stylus">
  ...
</style>

vuex 處理 token 值

  1. mutations.js 中晦嵌,當(dāng)存入用戶信息時(shí)
    • 一份存入 session 同辣,供日常使用
    • 一份存入 cookie 拷姿,有效期 30 天,供自動(dòng)登錄使用邑闺,具體的 cookie 存值方式跌前,請(qǐng)參見 簡(jiǎn)單封裝瀏覽器 cookie 工具類
  2. 同時(shí)需要確保,當(dāng)用戶手動(dòng)退出后執(zhí)行用戶信息清除時(shí)陡舅,需要同時(shí)清除 cookie
/** vuex所有的mutation */

// 引入mutations-types
import * as types from './mutation-types'
import { sessionStorage, cookieStorage } from 'assets/scripts/storage'

// 定義mutation抵乓,其內(nèi)部是一些修改方法
const mutations = {
  // 第一個(gè)參數(shù)是狀態(tài)值
  // 第二個(gè)參數(shù)為提交狀態(tài)修改是傳入的對(duì)象參數(shù)
  [types.SET_USER_INFO](state, userInfo) {
    state.userInfo = userInfo || {}

    if (userInfo === null) {
      sessionStorage.remove('userInfo')
      cookieStorage.remove('userInfo')
    } else {
      sessionStorage.set('userInfo', userInfo)
      cookieStorage.set('userInfo', userInfo, 30)
    }
  }
    ...
}

// 暴露給外部
export default mutations

在請(qǐng)求首頁時(shí)判斷是否可以自動(dòng)登錄

  1. router.js 的最后添加 router.beforeEach() ,可以在鏈接跳轉(zhuǎn)之前執(zhí)行一些預(yù)判操作
  2. 如果 cookie 中存在用戶登錄信息靶衍,則將信息取出重新放入 vuex 灾炭,并在后續(xù)操作中直接跳轉(zhuǎn)至登錄后的首屏頁面
/**
 * 全局路由配置
 * 路由開始之前的操作
 */
router.beforeEach((to, from, next) => {
  // 獲取當(dāng)前請(qǐng)求的名稱
  // let toName = to.name
  // 獲取當(dāng)前請(qǐng)求的路徑
  let toPath = to.path

  // 請(qǐng)求首頁時(shí)判斷用戶是否存在本地登錄信息
  if (toPath.indexOf('portal') === 1) {
    // 獲取用戶信息
    let userInfo = cookieStorage.get('userInfo')

    // 存在用戶信息
    if (userInfo !== undefined && userInfo.token !== undefined) {
      // 將用戶信息重新進(jìn)行狀態(tài)管理
      store.commit(SET_USER_INFO, userInfo)
    }
  }

  // 獲取用戶登錄標(biāo)識(shí)
  let isLogin = store.state.userInfo.isLogin

  // 用戶未登錄,且請(qǐng)求的不是首頁或首頁子頁面
  if (!isLogin && toPath.indexOf('portal') !== 1) {
    // 跳轉(zhuǎn)到登錄頁面
    next({
      name: 'portal'
    })
  } else {
    // 用戶已登錄颅眶,且請(qǐng)求的是登錄頁面
    if (isLogin && toPath.indexOf('portal') === 1) {
      // 跳轉(zhuǎn)到首頁
      next({
        path: serverBaseUrl
      })
    } else {
      // 默認(rèn)操作
      next()
    }
  }
})
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜈出,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子涛酗,更是在濱河造成了極大的恐慌铡原,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件商叹,死亡現(xiàn)場(chǎng)離奇詭異燕刻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)剖笙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門卵洗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弥咪,你說我怎么就攤上這事过蹂′毯粒” “怎么了息拜?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵祟辟,是天一觀的道長(zhǎng)盲赊。 經(jīng)常有香客問我候生,道長(zhǎng)嗤无,這世上最難降的妖魔是什么看尼? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任蹬音,我火速辦了婚禮坦报,結(jié)果婚禮上库说,老公的妹妹穿的比我還像新娘。我一直安慰自己片择,他們只是感情好潜的,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著字管,像睡著了一般啰挪。 火紅的嫁衣襯著肌膚如雪信不。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天亡呵,我揣著相機(jī)與錄音抽活,去河邊找鬼。 笑死锰什,一個(gè)胖子當(dāng)著我的面吹牛下硕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播汁胆,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼梭姓,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了嫩码?” 一聲冷哼從身側(cè)響起誉尖,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎铸题,沒想到半個(gè)月后铡恕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丢间,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年探熔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片千劈。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖牌捷,靈堂內(nèi)的尸體忽然破棺而出墙牌,到底是詐尸還是另有隱情,我是刑警寧澤暗甥,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布喜滨,位于F島的核電站,受9級(jí)特大地震影響撤防,放射性物質(zhì)發(fā)生泄漏虽风。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一寄月、第九天 我趴在偏房一處隱蔽的房頂上張望辜膝。 院中可真熱鬧,春花似錦漾肮、人聲如沸厂抖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忱辅。三九已至七蜘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間墙懂,已是汗流浹背橡卤。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留损搬,地道東北人碧库。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像场躯,于是被迫代替她去往敵國(guó)和親谈为。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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

  • 引子 去法國(guó)留學(xué)之前,徐悲鴻曾做了一對(duì)水晶戒指:一只上刻著“悲鴻”签舞,一只上鐫著“碧薇”秕脓。他把碧薇的戒指整天戴著,有...
    雨沉香閱讀 2,432評(píng)論 10 18
  • 編輯:劉旭冬 第六屆蘭州新區(qū)中川牡丹節(jié)5月18號(hào)(周五)盛大開園儒搭。 牡丹節(jié)期間的新區(qū)還有更大的彩蛋在等你吠架,二號(hào)湖音...
    旭冬lzu閱讀 206評(píng)論 0 0
  • 昨天,一個(gè)許久未聯(lián)系的朋友突然打電話給我搂鲫,我一看傍药,是個(gè)陌生號(hào)碼,南京的魂仍,猶豫了一下拐辽,但看到360并沒有攔截,就接了...
    孤筆客閱讀 432評(píng)論 0 1
  • 早晨白茫茫的世界擦酌,沒有其它的色彩俱诸,酒店的大廳更是寂靜。前臺(tái)的服務(wù)員還沒有上班赊舶。也是睁搭,7:00鐘太早了些。昨天入住的...
    溫花豆兒閱讀 281評(píng)論 0 0