Java公眾號(hào)登錄

背景

很久沒(méi)寫(xiě)博客了,寫(xiě)點(diǎn)跟公眾號(hào)相關(guān)的吧,相信大家一定都見(jiàn)過(guò),一個(gè)網(wǎng)站,點(diǎn)擊登錄按鈕,會(huì)出現(xiàn)微信掃碼登錄或者手機(jī)賬號(hào)密碼登錄,而點(diǎn)擊微信掃碼登錄,會(huì)出現(xiàn)一張二維碼,掃描這個(gè)二維碼,然后跳轉(zhuǎn)到相應(yīng)的公眾號(hào),點(diǎn)擊關(guān)注之后才能登錄成功,這樣能很好的給公眾號(hào)進(jìn)行導(dǎo)流,這里我們說(shuō)的就是微信掃碼,跳轉(zhuǎn)到公眾號(hào),關(guān)注之后再進(jìn)行登錄。

先叨叨兩句

這里先放上官方文檔地址:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html,個(gè)人認(rèn)為公眾號(hào)文檔還是寫(xiě)的比小程序好的。開(kāi)始之前建議先通讀下這三個(gè)tab的文檔:

圖1

然后我們?nèi)ノ⑿殴娞?hào)設(shè)置頁(yè)面:開(kāi)發(fā)->基本配置下設(shè)置開(kāi)發(fā)者密碼,把你本地和開(kāi)發(fā)環(huán)境IP白名單配置好,然后再去設(shè)置服務(wù)器配置:填寫(xiě)服務(wù)器回調(diào)地址,令牌,消息加密秘鑰,消息加密方式為:安全模式,這一步配置微信會(huì)回調(diào)你的配置的接口,所以要先準(zhǔn)備好回調(diào)接口,不然無(wú)法成功。準(zhǔn)備好之后,下面進(jìn)入開(kāi)發(fā)。

需要的額外包

因?yàn)樯弦黄恼麓蠹叶颊f(shuō)jar包不知道是哪個(gè),這次我都詳細(xì)列出來(lái)了,其實(shí)只要稍微用點(diǎn)心應(yīng)該是知道的,重要的是思路,而不是只Ctrl+c,Ctrl+v

<dependency>
   <groupId>com.github.liyiorg</groupId>
   <artifactId>weixin-popular</artifactId>
   <version>2.8.28</version>
</dependency>
<dependency>
   <groupId>dom4j</groupId>
   <artifactId>dom4j</artifactId>
   <version>1.6.1</version>
</dependency>
<dependency>
   <groupId>com.thoughtworks.xstream</groupId>
   <artifactId>xstream</artifactId>
   <version>1.4.11.1</version>
</dependency>
<dependency>
   <groupId>com.github.liyiorg</groupId>
   <artifactId>weixin-popular</artifactId>
   <version>2.8.28</version>
</dependency>

配置文件

在你的項(xiàng)目的全局配置文件.yml中加入如下配置:

wechat:
 subscription:
   auth:
     appid: 你的APPID
     secret: 你的secret
     token: 你的token
     encodingAesKey: 你的消息加密秘鑰

正式開(kāi)始

寫(xiě)一個(gè)登錄的controller:

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@Api(value = "wechatSubscriptionLogin", tags = "微信公眾號(hào)登錄相關(guān)接口")
@RequestMapping(value = "/wechat-subscription/login")
@RestController(value = "WeChatSubscriptionLoginController")
public class WeChatSubscriptionLoginController{
    @Value("${wechat.subscription.auth.token}")
    private String token;
    
    @Resource
    WechatService wechatService;
    
    private final Logger logger = LoggerFactory.getLogger(WeChatSubscriptionLoginController.class);
    
    @ApiOperation(value = "獲取登錄二維碼", httpMethod = "GET")
    @GetMapping("/ticket")
    public String getTicket(@RequestParam String sceneStr) throws Exception {
        String result = wechatService.getTicket(sceneStr);
        return result;
    }

    @ApiOperation(value = "檢查是否登錄", httpMethod = "GET")
    @GetMapping("/check-login")
    public UserInfoDTO checkLogin(@RequestParam String sceneStr) throws Exception {
        UserInfoDTO userInfoDTO = wechatService.checkLoginReturnToken(sceneStr);
        return userInfoDTO;
    }

    @ApiOperation(value = "微信回調(diào)", httpMethod = "GET")
    @GetMapping("/callback")
    public void checkWechat(HttpServletRequest request, HttpServletResponse response) throws Exception {
        logger.info("進(jìn)入回調(diào)get方法");
        // 微信加密簽名
        String signature = request.getParameter("signature");
        // 時(shí)間戳
        String timestamp = request.getParameter("timestamp");
        // 隨機(jī)數(shù)
        String nonce = request.getParameter("nonce");
        // 隨機(jī)字符串
        String echostr = request.getParameter("echostr");

        PrintWriter out = response.getWriter();

        logger.info("參數(shù)為,signature:" + signature + ",timestamp:" +
                timestamp + ",nonce" + nonce + ",echostr" + echostr);

        // 通過(guò)檢驗(yàn)signature對(duì)請(qǐng)求進(jìn)行校驗(yàn),若校驗(yàn)成功則原樣返回echostr,表示接入成功,否則接入失敗
        if (SignUtil.checkSignature(signature, timestamp, nonce, token)) {
            logger.info("驗(yàn)簽成功");
            out.print(echostr);
        }
        out.close();
    }

    @ApiOperation(value = "微信回調(diào)", httpMethod = "POST")
    @PostMapping("/callback")
    public void callBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 微信加密簽名
        String signature = request.getParameter("msg_signature");
        // 時(shí)間戳
        String timestamp = request.getParameter("timestamp");
        // 隨機(jī)數(shù)
        String nonce = request.getParameter("nonce");

        wechatService.callBack(request.getInputStream(), response.getWriter(), signature, timestamp, nonce);
    }
}

這里我們可以看到最后有兩個(gè)/callback接口,一個(gè)是get請(qǐng)求,用于文章開(kāi)頭所說(shuō)的設(shè)置服務(wù)器配置,所以這個(gè)接口要先發(fā)布上線(xiàn),才能配置成功,另一個(gè)是post請(qǐng)求,這個(gè)就是用于公眾號(hào)接收到用戶(hù)的動(dòng)作之后回調(diào)我們的接口。另外兩個(gè)接口暫時(shí)不用管,我們回過(guò)頭再說(shuō)。其中:signutil工具類(lèi)的代碼為:

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

/**
 * @author luoling
 * @date 2020-04-27 11:53
 */
public class SignUtil {
    public static boolean checkSignature(String signature, String timestamp,
                                         String nonce, String token) {
        // 1.將token、timestamp、nonce三個(gè)參數(shù)進(jìn)行字典序排序
        String[] arr = new String[]{token, timestamp, nonce};
        Arrays.sort(arr);

        // 2. 將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行sha1加密
        StringBuilder content = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            content.append(arr[i]);
        }
        MessageDigest md = null;
        String tmpStr = null;
        try {
            md = MessageDigest.getInstance("SHA-1");
            // 將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行sha1加密
            byte[] digest = md.digest(content.toString().getBytes());
            tmpStr = byteToStr(digest);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        // 3.將sha1加密后的字符串可與signature對(duì)比,標(biāo)識(shí)該請(qǐng)求來(lái)源于微信
        return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
    }

    private static String byteToStr(byte[] byteArray) {
        StringBuilder strDigest = new StringBuilder();
        for (int i = 0; i < byteArray.length; i++) {
            strDigest.append(byteToHexStr(byteArray[i]));
        }
        return strDigest.toString();
    }

    private static String byteToHexStr(byte mByte) {
        char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A',
                'B', 'C', 'D', 'E', 'F'};
        char[] tempArr = new char[2];
        tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
        tempArr[1] = Digit[mByte & 0X0F];
        String s = new String(tempArr);
        return s;
    }
}
AccessToken簡(jiǎn)介

到這里,微信就能跟你進(jìn)行通信了,然后我們?cè)僬f(shuō)說(shuō)AccessToken這個(gè)東西,AccessToken在公眾號(hào)的文檔中說(shuō)的很清楚了,放上地址:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html。看這個(gè)就夠了,簡(jiǎn)單來(lái)說(shuō),調(diào)用微信的接口都需要傳遞AccessToken。官方文檔建議我們通過(guò)主動(dòng)和被動(dòng)來(lái)獲取AccessToken,主動(dòng)的話(huà)就是用定時(shí)任務(wù)來(lái)刷新AccessToken,被動(dòng)就是當(dāng)AccessToken過(guò)期的時(shí)候,在業(yè)務(wù)中維護(hù)獲取AccessToken的方法。文檔上說(shuō)AccessToken過(guò)期時(shí)間是兩個(gè)小時(shí),但是以我實(shí)際開(kāi)發(fā)中遇到的問(wèn)題來(lái)說(shuō),定時(shí)任務(wù)每5min去刷新就好,不要卡在2個(gè)小時(shí)這個(gè)時(shí)間點(diǎn)上。而且AccessToken是全局唯一的,在緩存中存一份就好,如果沒(méi)有測(cè)試公眾號(hào),那么這個(gè)緩存要在任意環(huán)境都能訪(fǎng)問(wèn),不論是prod還是Dev還是gray。

處理用戶(hù)操作回調(diào)消息

接下來(lái),當(dāng)用戶(hù)對(duì)該公眾號(hào)的任何操作,都會(huì)由微信通過(guò)POST的回調(diào)接口回調(diào)消息給你,也就是我們登陸controller中的/callback POST接口,你只需要在業(yè)務(wù)代碼中對(duì)應(yīng)處理就好,這里包含了用戶(hù)登錄業(yè)務(wù),大致代碼如下:

import com.alibaba.fastjson.JSONObject;
import weixin.popular.bean.user.User;
import weixin.popular.api.UserAPI;

@Override
    public void callBack(InputStream inputStream, PrintWriter printWriter,
                         String signature, String timestamp, String nonce) throws Exception {
        logger.info("callback被調(diào)用");
        Map<String, String> messageMap = MessageUtil.parseXmlCrypt(inputStream,
                MessageUtil.getWXBizMsgCrypt(subscriptionToken, subscriptionEncodingAesKey, subscriptionAppid),
                signature, timestamp, nonce);
        logger.info("messageMap為:" + JSONObject.toJSONString(messageMap));
        ReceiveMessageDTO receiveMessageDTO = MessageUtil.mapToBean(messageMap);
        logger.info("參數(shù)為,receiveMessageDO:" + JSONObject.toJSONString(receiveMessageDTO));
        // 根據(jù)openID,請(qǐng)求微信獲取用戶(hù)信息
        String accessToken = youGetAccessTokenMethod();
                User user = UserAPI.userInfo(accessToken, subscriptionOpenId);
        logger.info("從微信獲取用戶(hù)的數(shù)據(jù)為:" + JSONObject.toJSONString(user));
        String responseMessage = "";
                // 簡(jiǎn)單展示兩個(gè)類(lèi)型消息,具體消息類(lèi)型可以看文檔
        try {
            switch (receiveMessageDTO.getMsgType()) {
                case MessageUtil.MESSAGE_EVENT:
                    logger.info("進(jìn)入event事件");
                    // 用戶(hù)關(guān)注公眾號(hào)是event事件,這里處理用戶(hù)關(guān)注公眾號(hào)或者已關(guān)注掃碼登錄的邏輯,具體處理根據(jù)自己業(yè)務(wù)來(lái)定
                    // 當(dāng)業(yè)務(wù)成功的獲取到了用戶(hù)的信息時(shí),可以以sceneStr為key,把用戶(hù)信息放入緩存中,
                    // 在/check-login中給到前端,這時(shí)前端知道用戶(hù)已經(jīng)登錄了,就可以跳轉(zhuǎn)到業(yè)務(wù)頁(yè)面了
                        
                    // saveUserAndSaveUserTokenInCache();
                    break;
                case MessageUtil.MESSAGE_TEXT:
                    logger.info("進(jìn)入text事件");
                    break;
                default:
                    responseMessage = "";
            }
        }catch (Exception e) {
            responseMessage = "";
            logger.error("微信公眾號(hào)回調(diào)異常", e);
        }
        // 回復(fù)信息給到關(guān)注 & 登錄者
        logger.info("回復(fù)的消息為:" + responseMessage);
        // 消息加密,如果消息不為空,則回復(fù),如果消息為空,不回復(fù)
        if (responseMessage == null) {
            responseMessage = "";
        }
        responseMessage = MessageUtil.getWXBizMsgCrypt(subscriptionToken, subscriptionEncodingAesKey, subscriptionAppid)
                .encryptMsg(responseMessage, timestamp, nonce);
        logger.info("加密后的消息為:" + responseMessage);
        try {
            printWriter.print(responseMessage);
        } catch (Exception e) {
            throw e;
        } finally {
            if (printWriter != null) {
                printWriter.close();
            }
        }
    }

其中工具類(lèi)和DTO的代碼如下:

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class ReceiveMessageDTO {
    // 開(kāi)發(fā)者微信號(hào)
    private String toUserName;

    // 發(fā)送方openID
    private String fromUserName;

    private Integer createTime;

    private String msgType;

    private String event;

    private String content;

    // 事件key值
    private String eventKey;

    private String ticket;
}

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * @author luoling
 * @Description 這里必須要首字母大寫(xiě),需要轉(zhuǎn)成xml
 * @date 2020-04-27 16:19
 */
@Getter
@Setter
@ToString
public class SendTextMessageDTO {
    private String ToUserName;

    private String FromUserName;

    private Integer CreateTime;

    private String MsgType;

    private String Content;
}

import com.qq.weixin.mp.aes.WXBizMsgCrypt;
import com.thoughtworks.xstream.XStream;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import weixin.popular.bean.message.templatemessage.TemplateMessageItem;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @author luoling
 * @date 2020-04-27 14:52
 */
public class MessageUtil {
    public static final String MESSAGE_TEXT = "text";

    public static final String MESSAGE_IMAGE = "image";

    public static final String MESSAGE_VOICE = "voice";

    public static final String MESSAGE_VIDEO = "video";

    public static final String MESSAGE_SHORTVIDEO = "shortvideo";

    public static final String MESSAGE_LINK = "link";

    public static final String MESSAGE_LOCATION = "location";

    public static final String MESSAGE_EVENT = "event";

    public static final String MESSAGE_SUBSCRIBE = "subscribe";

    public static final String MESSAGE_UNSUBSCRIBE = "unsubscribe";

    public static final String MESSAGE_CLICK = "CLICK";

    public static final String MESSAGE_VIEW = "VIEW";

    public static final String MESSAGE_SCAN = "SCAN";

    public static final String MENU_CLICK = "click";

    public static final String MENU_MINIPROGRAM = "miniprogram";

    public static String initText(String toUserName, String fromUserName, String content) {
        SendTextMessageDTO message = new SendTextMessageDTO();
        // 接收方openID
        message.setToUserName(toUserName);
        // 開(kāi)發(fā)者微信號(hào)
        message.setFromUserName(fromUserName);
        message.setMsgType(MESSAGE_TEXT);
        message.setContent(content);
        message.setCreateTime((int) (System.currentTimeMillis() / 1000));
        return objectToXml(message);
    }

    public static ReceiveMessageDTO mapToBean(Map<String, String> map) {
        ReceiveMessageDTO receiveMessageDTO = new ReceiveMessageDTO();
        receiveMessageDTO.setEvent(map.get("Event"));
        receiveMessageDTO.setFromUserName(map.get("FromUserName"));
        receiveMessageDTO.setToUserName(map.get("ToUserName"));
        receiveMessageDTO.setMsgType(map.get("MsgType"));
        receiveMessageDTO.setContent(map.get("Content"));
        receiveMessageDTO.setCreateTime(Integer.valueOf(map.get("CreateTime")));
        String eventKey = map.get("EventKey");
        if (eventKey != null) {
            receiveMessageDTO.setEventKey(eventKey.replace("qrscene_", ""));
        }
        receiveMessageDTO.setTicket(map.get("Ticket"));
        return receiveMessageDTO;
    }

    /*將我們的消息內(nèi)容轉(zhuǎn)變?yōu)閤ml*/
    private static String objectToXml(SendTextMessageDTO message) {
        XStream xStream = new XStream();
        //xml根節(jié)點(diǎn)替換成<xml> 默認(rèn)是Message的包名
        xStream.alias("xml", message.getClass());
        return xStream.toXML(message);
    }
    
    public static Map<String, String> parseXmlCrypt(InputStream inputStream, WXBizMsgCrypt wxCeypt,
                                                    String msgSignature, String timestamp, String nonce) throws Exception {
        // 將解析結(jié)果存儲(chǔ)在HashMap中
        Map<String, String> map = new HashMap<>();

        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        String line;
        StringBuffer buf = new StringBuffer();
        while ((line = reader.readLine()) != null) {
            buf.append(line);
        }
        reader.close();
        inputStream.close();

        String respXml = wxCeypt.decryptMsg(msgSignature, timestamp, nonce, buf.toString());

        //SAXReader reader = new SAXReader();
        Document document = DocumentHelper.parseText(respXml);
        // 得到xml根元素
        Element root = document.getRootElement();
        // 得到根元素的所有子節(jié)點(diǎn)
        List<Element> elementList = root.elements();

        // 遍歷所有子節(jié)點(diǎn)
        for (Element e : elementList){
            map.put(e.getName(), e.getText());
        }

        return map;
    }

    public static WXBizMsgCrypt getWXBizMsgCrypt(String token, String encodingAesKey, String appid) throws Exception {
        return new WXBizMsgCrypt(token, encodingAesKey, appid);
    }

    public static LinkedHashMap<String, TemplateMessageItem> buildMessageDataMap(String first, String remark, String... keywords) {
        LinkedHashMap<String, TemplateMessageItem> dataMap = new LinkedHashMap<>();
        dataMap.put("first", new TemplateMessageItem(first, null));
        Integer count = 1;
        for (String keyword : keywords) {
            dataMap.put("keyword" + count++, new TemplateMessageItem(keyword, null));
        }
        dataMap.put("remark", new TemplateMessageItem(remark, null));
        return dataMap;
    }
}
前端如何處理

到這里,我們已經(jīng)處理了用戶(hù)掃碼---->點(diǎn)擊關(guān)注公眾號(hào)按鈕---->后端執(zhí)行登錄這個(gè)流程,接下來(lái)就是前端的處理了,前端這邊其實(shí)很簡(jiǎn)單,只需要先調(diào)用后端登錄接口中的/ticket接口獲取登錄二維碼,然后定時(shí)輪詢(xún):/check-login這個(gè)接口就行了,兩個(gè)接口實(shí)現(xiàn)如下:

import weixin.popular.bean.qrcode.QrcodeTicket;
import weixin.popular.api.*;
import java.net.URI;
import java.net.URLEncoder;
    // sceneStr是前端生成的隨機(jī)唯一字符串
    @Override
    public String getTicket(String sceneStr) throws Exception {
        // 自己編寫(xiě)獲取AccessToken方法
        String accessToken = getAccessTokenFromYouCache();
        // expireSeconds這里我寫(xiě)的是1min,根據(jù)自己業(yè)務(wù)修改,到期二維碼就失效
        QrcodeTicket qrcodeTicket = QrcodeAPI.qrcodeCreateTemp(accessToken, expireSeconds, sceneStr);
        if (qrcodeTicket == null || !qrcodeTicket.isSuccess() || StringUtils.isBlank(qrcodeTicket.getUrl())) {
            logger.info("獲取臨時(shí)二維碼報(bào)錯(cuò),json為:" + JSONObject.toJSONString(qrcodeTicket) + ",AccessToken為:" + accessToken);
            // 自定義ErrorCodeEnum,自己替換成自己的或者去掉
            throw new IllegalArgumentException(ErrorCodeEnum.WX06.getCode());
        }
        // subscriptionTicketUrl為:https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=,建議維護(hù)在配置文件中
        return subscriptionTicketUrl + URLEncoder.encode(qrcodeTicket.getTicket(), "utf-8");
    }
    
    @Override
    public UserInfoDTO checkLoginReturnToken(String sceneStr) {
        // 從緩存中獲取用戶(hù)信息
        getUserInfoFromCache();
        // 把緩存中的數(shù)據(jù)轉(zhuǎn)化成自定義的DTO給到前端,UserInfoDTO就是自定義DTO
    }

總結(jié)及相應(yīng)的流程圖

好了,到這里,整個(gè)流程就結(jié)束了,我們?cè)賮?lái)總結(jié)下,整個(gè)業(yè)務(wù)流程以接口為維度大致如下圖所示:


圖二

最后在放上相應(yīng)的二維碼和微信回調(diào)消息文檔:

獲取帶參數(shù)的二維碼:https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html

接收事件推送:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html

整個(gè)代碼基于業(yè)務(wù)有刪減,如果哪里刪出了問(wèn)題歡迎留言跟我說(shuō)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 177,128評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,291評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,421評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,642評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,821評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,053評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,896評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評(píng)論 2 375

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