ABP入門系列(16)——通過webapi與系統(tǒng)進(jìn)行交互

ABP入門系列目錄——學(xué)習(xí)Abp框架之實(shí)操演練
源碼路徑:Github-LearningMpaAbp


1. 引言

上一節(jié)我們講解了如何創(chuàng)建微信公眾號模塊,這一節(jié)我們就繼續(xù)跟進(jìn)琼锋,來講一講公眾號模塊如何與系統(tǒng)進(jìn)行交互缕坎。
微信公眾號模塊作為一個獨(dú)立的web模塊部署谜叹,要想與現(xiàn)有的【任務(wù)清單】進(jìn)行交互搬葬,我們要想明白以下幾個問題:

  1. 如何進(jìn)行交互踩萎?
    ABP模板項(xiàng)目中默認(rèn)創(chuàng)建了webapi項(xiàng)目香府,其動態(tài)webapi技術(shù)允許我們直接訪問appservice作為webapi而不用在webapi層編寫額外的代碼码倦。所以袁稽,自然而然我們要通過webapi與系統(tǒng)進(jìn)行交互。
  2. 通過webapi與系統(tǒng)進(jìn)行交互歧沪,如何確保安全莲组?
    我們知道暴露的webapi如果不加以授權(quán)控制锹杈,就如同在大街上裸奔竭望。所以在訪問webapi時咬清,我們需要通過身份認(rèn)證來確保安全訪問。
  3. 都有哪幾種身份認(rèn)證方式喻圃?
    第一種就是大家熟知的cookie認(rèn)證方式斧拍;
    第二種就是token認(rèn)證方式:在訪問webapi之前肆汹,先要向目標(biāo)系統(tǒng)申請令牌(token)予权,申請到令牌后扫腺,再使用令牌訪問webapi笆环。Abp默認(rèn)提供了這種方式躁劣;
    第三種是基于OAuth2.0的token認(rèn)證方式:OAuth2.0是什么玩意?建議先看看OAuth2.0 知多少以便我們后續(xù)內(nèi)容的展開志膀。OAuth2.0認(rèn)證方式彌補(bǔ)了Abp自帶token認(rèn)證的短板溉浙,即無法進(jìn)行token刷新戳稽。

基于這一節(jié)广鳍,我完善了一個demo,大家可以直接訪問http://shengjietest.azurewebsites.net/進(jìn)行體驗(yàn)吨铸。

demo

下面我們就以【通過webapi請求用戶列表】為例看一看三種認(rèn)證方式的具體實(shí)現(xiàn)。

2. Cookie認(rèn)證方式

Cookie認(rèn)證方式的原理就是:在訪問webapi之前竭缝,通過登錄目標(biāo)系統(tǒng)建立連接抬纸,將cookie寫入本地湿故。下一次訪問webapi的時候攜帶cookie信息就可以完成認(rèn)證坛猪。

2.1. 登錄目標(biāo)系統(tǒng)

這一步簡單,我們僅需提供用戶名密碼命黔,Post一個登錄請求即可悍募。
我們在微信模塊中創(chuàng)建一個WeixinController

public class WeixinController : Controller
{
    private readonly IAbpWebApiClient _abpWebApiClient;
    private string baseUrl = "http://shengjie.azurewebsites.net/";
    private string loginUrl = "/account/login";
    private string webapiUrl = "/api/services/app/User/GetUsers";
    private string abpTokenUrl = "/api/Account/Authenticate";
    private string oAuthTokenUrl = "/oauth/token";
    private string user = "admin";
    private string pwd = "123qwe";

    public WeixinController()
    {
        _abpWebApiClient = new AbpWebApiClient();
    }
}

其中IAbpWebApiClient是對HttpClient的封裝搜立,用于發(fā)送 HTTP 請求和接收HTTP 響應(yīng)啄踊。

下面添加CookieBasedAuth方法颠通,來完成登錄認(rèn)證顿锰,代碼如下:

public async Task CookieBasedAuth()
{
    Uri uri = new Uri(baseUrl + loginUrl);
    var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None, UseCookies = true };

    using (var client = new HttpClient(handler))
    {
        client.BaseAddress = uri;
        client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

        var content = new FormUrlEncodedContent(new Dictionary<string, string>()
        {
            {"TenancyName", "Default"},
            {"UsernameOrEmailAddress", user},
            {"Password", pwd }
        });
                
        var result = await client.PostAsync(uri, content);

        string loginResult = await result.Content.ReadAsStringAsync();

        var getCookies = handler.CookieContainer.GetCookies(uri);

        foreach (Cookie cookie in getCookies)
        {
            _abpWebApiClient.Cookies.Add(cookie);
        }
    }
}

這段代碼中有幾個點(diǎn)需要注意:

  1. 指定HttpClientHandler屬性UseCookie = true硼控,使用Cookie牢撼;
  2. client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));用來指定接受的返回值熏版;
  3. 使用FormUrlEncodedContent進(jìn)行傳參撼短;
  4. 使用var getCookies = handler.CookieContainer.GetCookies(uri);獲取返回的Cookie挺勿,并添加到_abpWebApiClient.Cookies的集合中不瓶,以便下次直接攜帶cookie信息訪問webapi湃番。

2.2. 攜帶cookie訪問webapi

服務(wù)器返回的cookie信息在登錄成功后已經(jīng)填充到_abpWebApiClient.Cookies中吠撮,我們只需post一個請求到目標(biāo)api即可泥兰。

public async Task<PartialViewResult> SendRequestBasedCookie()
{
    await CookieBasedAuth();
    return await GetUserList(baseUrl + webapiUrl);
}

private async Task<PartialViewResult> GetUserList(string url)
{
    try
    {
        var users = await _abpWebApiClient.PostAsync<ListResultDto<UserListDto>>(url);

        return PartialView("_UserListPartial", users.Items);
    }
    catch (Exception e)
    {
        ViewBag.ErrorMessage = e.Message;
    }

    return null;
}

3. Token認(rèn)證方式

Abp默認(rèn)提供的token認(rèn)證方式鞋诗,很簡單削彬,我們僅需要post一個請求到/api/Account/Authenticate即可請求到token。然后使用token即可請求目標(biāo)webapi壶笼。
但這其中有一個問題就是覆劈,如果token過期责语,就必須使用用戶名密碼重寫申請token坤候,體驗(yàn)不好铐拐。

3.1. 請求token

public async Task<string> GetAbpToken()
{
    var tokenResult = await _abpWebApiClient.PostAsync<string>(baseUrl + abpTokenUrl, new
    {
        TenancyName = "Default",
        UsernameOrEmailAddress = user,
        Password = pwd
    });
    this.Response.SetCookie(new HttpCookie("access_token", tokenResult));
    return tokenResult;
}

這段代碼中我們將請求到token直接寫入到cookie中遍蟋。以便我們下次直接從cookie中取回token直接訪問webapi虚青。

3.2. 使用token訪問webapi

從cookie中取回token棒厘,在請求頭中添加Authorization = Bearer token,即可谓媒。

public async Task<PartialViewResult> SendRequest()
{
    var token = Request.Cookies["access_token"]?.Value;
    //將token添加到請求頭
    _abpWebApiClient.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token));

    return await GetUserList(baseUrl + webapiUrl);
}

這里面需要注意的是句惯,abp中配置app.UseOAuthBearerAuthentication(AccountController.OAuthBearerOptions);使用的是Bearer token抢野,所以我們在請求weiapi時指孤,要在請求頭中假如Authorization信息時恃轩,使用Bearer token的格式傳輸token信息(Bearer后有一個空格详恼!)昧互。

4. OAuth2.0 Token認(rèn)證方式

OAuth2.0提供了token刷新機(jī)制伟桅,當(dāng)服務(wù)器頒發(fā)的token過期后楣铁,我們可以直接通過refresh_token來申請token即可盖腕,不需要用戶再錄入用戶憑證申請token溃列。

4.1. Abp集成OAuth2.0

在WebApi項(xiàng)目中的Api路徑下創(chuàng)建Providers文件夾听隐,添加SimpleAuthorizationServerProviderSimpleRefreshTokenProvider類雅任。
其中SimpleAuthorizationServerProvider用來驗(yàn)證客戶端的用戶名和密碼來頒發(fā)token沪么;SimpleRefreshTokenProvider用來刷新token禽车。

public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider, ITransientDependency
{
    private readonly LogInManager _logInManager;

    public SimpleAuthorizationServerProvider(LogInManager logInManager)
        {
            _logInManager = logInManager;
        }

    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            string clientId;
            string clientSecret;
            if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
            {
                context.TryGetFormCredentials(out clientId, out clientSecret);
            }
            var isValidClient = string.CompareOrdinal(clientId, "app") == 0 &&
                                string.CompareOrdinal(clientSecret, "app") == 0;
            if (isValidClient)
            {
                context.OwinContext.Set("as:client_id", clientId);
                context.Validated(clientId);
            }
            else
            {
                context.SetError("invalid client");
            }

            return Task.FromResult<object>(null);
        }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            var tenantId = context.Request.Query["tenantId"];
            var result = await GetLoginResultAsync(context, context.UserName, context.Password, tenantId);
            if (result.Result == AbpLoginResultType.Success)
            {
                //var claimsIdentity = result.Identity;                
                var claimsIdentity = new ClaimsIdentity(result.Identity);
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
                var ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
                context.Validated(ticket);
            }
        }

    public override  Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
        {
            var originalClient = context.OwinContext.Get<string>("as:client_id");
            var currentClient = context.ClientId;

            // enforce client binding of refresh token
            if (originalClient != currentClient)
            {
                context.Rejected();
                return Task.FromResult<object>(null);
            }

            // chance to change authentication ticket for refresh token requests
            var newId = new ClaimsIdentity(context.Ticket.Identity);
            newId.AddClaim(new Claim("newClaim", "refreshToken"));

            var newTicket = new AuthenticationTicket(newId, context.Ticket.Properties);
            context.Validated(newTicket);

            return Task.FromResult<object>(null);
        }

    private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(OAuthGrantResourceOwnerCredentialsContext context,
        string usernameOrEmailAddress, string password, string tenancyName)
        {
            var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);

            switch (loginResult.Result)
            {
                case AbpLoginResultType.Success:
                    return loginResult;
                default:
                    CreateExceptionForFailedLoginAttempt(context, loginResult.Result, usernameOrEmailAddress, tenancyName);
                    //throw CreateExceptionForFailedLoginAttempt(context,loginResult.Result, usernameOrEmailAddress, tenancyName);
                    return loginResult;
            }
        }

    private void CreateExceptionForFailedLoginAttempt(OAuthGrantResourceOwnerCredentialsContext context, 
        AbpLoginResultType result, string usernameOrEmailAddress, string tenancyName)
        {
            switch (result)
            {
                case AbpLoginResultType.Success:
                    throw new ApplicationException("Don't call this method with a success result!");
                case AbpLoginResultType.InvalidUserNameOrEmailAddress:
                case AbpLoginResultType.InvalidPassword:
                    context.SetError(L("LoginFailed"), L("InvalidUserNameOrPassword"));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), ("InvalidUserNameOrPassword"));
                case AbpLoginResultType.InvalidTenancyName:
                    context.SetError(L("LoginFailed"), L("ThereIsNoTenantDefinedWithName", tenancyName));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), string.Format("ThereIsNoTenantDefinedWithName{0}", tenancyName));
                case AbpLoginResultType.TenantIsNotActive:
                    context.SetError(L("LoginFailed"), L("TenantIsNotActive", tenancyName));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), string.Format("TenantIsNotActive {0}", tenancyName));
                case AbpLoginResultType.UserIsNotActive:
                    context.SetError(L("LoginFailed"), L("UserIsNotActiveAndCanNotLogin", usernameOrEmailAddress));
                    break;
                //    return new UserFriendlyException(("LoginFailed"), string.Format("UserIsNotActiveAndCanNotLogin {0}", usernameOrEmailAddress));
                case AbpLoginResultType.UserEmailIsNotConfirmed:
                    context.SetError(L("LoginFailed"), L("UserEmailIsNotConfirmedAndCanNotLogin"));
                    break;
                    //    return new UserFriendlyException(("LoginFailed"), ("UserEmailIsNotConfirmedAndCanNotLogin"));
                    //default: //Can not fall to default actually. But other result types can be added in the future and we may forget to handle it
                    //    //Logger.Warn("Unhandled login fail reason: " + result);
                    //    return new UserFriendlyException(("LoginFailed"));
            }
        }

    private static string L(string name, params object[] args)
        {
            //return new LocalizedString(name);
            return IocManager.Instance.Resolve<ILocalizationService>().L(name, args);
        }
}
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider, ITransientDependency
{
    private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();

    public Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            var guid = Guid.NewGuid().ToString("N");

            // maybe only create a handle the first time, then re-use for same client
            // copy properties and set the desired lifetime of refresh token
            var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
            {
                IssuedUtc = context.Ticket.Properties.IssuedUtc,
                ExpiresUtc = DateTime.UtcNow.AddYears(1)
            };
            var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);

            //_refreshTokens.TryAdd(guid, context.Ticket);
            _refreshTokens.TryAdd(guid, refreshTokenTicket);

            // consider storing only the hash of the handle
            context.SetToken(guid);

            return Task.FromResult<object>(null);
        }

    public Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {
            AuthenticationTicket ticket;
            if (_refreshTokens.TryRemove(context.Token, out ticket))
            {
                context.SetTicket(ticket);
            }

            return Task.FromResult<object>(null);
        }

    public void Create(AuthenticationTokenCreateContext context)
        {
            throw new NotImplementedException();
        }

    public void Receive(AuthenticationTokenReceiveContext context)
        {
            throw new NotImplementedException();
        }
}

以上兩段代碼我就不做過多解釋彻采,請自行走讀捌归。

緊接著我們在Api目錄下創(chuàng)建OAuthOptions類用來配置OAuth認(rèn)證惜索。

public class OAuthOptions
{
    /// <summary>
    /// Gets or sets the server options.
    /// </summary>
    /// <value>The server options.</value>
    private static OAuthAuthorizationServerOptions _serverOptions;

    /// <summary>
    /// Creates the server options.
    /// </summary>
    /// <returns>OAuthAuthorizationServerOptions.</returns>
    public static OAuthAuthorizationServerOptions CreateServerOptions()
    {
        if (_serverOptions == null)
        {
            var provider = IocManager.Instance.Resolve<SimpleAuthorizationServerProvider>();
            var refreshTokenProvider = IocManager.Instance.Resolve<SimpleRefreshTokenProvider>();
            _serverOptions = new OAuthAuthorizationServerOptions
            {
                TokenEndpointPath = new PathString("/oauth/token"),
                Provider = provider,
                RefreshTokenProvider = refreshTokenProvider,
                AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(30),
                AllowInsecureHttp = true
            };
        }
        return _serverOptions;
    }
}

從中我們可以看出,主要配置了以下幾個屬性:

  • TokenEndpointPath :用來指定請求token的路由角塑;
  • Provider:用來指定創(chuàng)建token的Provider圃伶;
  • RefreshTokenProvider:用來指定刷新token的Provider蒲列;
  • AccessTokenExpireTimeSpan :用來指定token過期時間蝗岖,這里我們指定了30s剪侮,是為了demo 如何刷新token瓣俯。
  • AllowInsecureHttp:用來指定是否允許http連接彩匕。

創(chuàng)建上面三個類之后驼仪,我們需要回到Web項(xiàng)目的Startup類中,配置使用集成的OAuth2.0湾碎,代碼如下:

public void Configuration(IAppBuilder app)
{
    //第一步:配置跨域訪問
    app.UseCors(CorsOptions.AllowAll);

    app.UseOAuthBearerAuthentication(AccountController.OAuthBearerOptions);

    //第二步:使用OAuth密碼認(rèn)證模式
    app.UseOAuthAuthorizationServer(OAuthOptions.CreateServerOptions());

    //第三步:使用Abp
    app.UseAbp();
    
    //省略其他代碼
}

其中配置跨越訪問時,我們需要安裝Microsoft.Owin.CorsNuget包。

至此溢陪,Abp集成OAuth的工作完成了形真。

4.2. 申請OAuth token

我們在Abp集成OAuth配置的申請token的路由是/oauth/token咆霜,所以我們將用戶憑證post到這個路由即可申請token:

public async Task<string> GetOAuth2Token()
{
    Uri uri = new Uri(baseUrl + oAuthTokenUrl);
    var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None };

    using (var client = new HttpClient(handler))
    {
        client.BaseAddress = uri;
        client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

        var content = new FormUrlEncodedContent(new Dictionary<string, string>()
        {
            {"grant_type", "password"},
            {"username", user },
            {"password", pwd },
            {"client_id", "app" },
            {"client_secret", "app"},
        });

        //獲取token保存到cookie裕便,并設(shè)置token的過期日期                    
        var result = await client.PostAsync(uri, content);
        string tokenResult = await result.Content.ReadAsStringAsync();

        var tokenObj = (JObject)JsonConvert.DeserializeObject(tokenResult);
        string token = tokenObj["access_token"].ToString();
        string refreshToken = tokenObj["refresh_token"].ToString();
        long expires = Convert.ToInt64(tokenObj["expires_in"]);

        this.Response.SetCookie(new HttpCookie("access_token", token));
        this.Response.SetCookie(new HttpCookie("refresh_token", refreshToken));
        this.Response.Cookies["access_token"].Expires = Clock.Now.AddSeconds(expires);

        return tokenResult;
    }
}

在這段代碼中我們指定的grant_type = password挂疆,這說明我們使用的是OAuth提供的密碼認(rèn)證模式缤言。其中{"client_id", "app" }, {"client_secret", "app"}(搞過微信公眾號開發(fā)的應(yīng)該對這個很熟悉)用來指定客戶端的身份和密鑰胆萧,這邊我們直接寫死庆揩。
通過OAuth的請求的token主要包含四部分:

  • token:令牌
  • refreshtoken:刷新令牌
  • expires_in:token有效期
  • token_type:令牌類型,我們這里是Bearer

為了演示方便跌穗,我們直接把token信息直接寫入到cookie中订晌,實(shí)際項(xiàng)目中建議寫入數(shù)據(jù)庫。

4.3. 刷新token

如果我們的token過期了怎么辦蚌吸,咱們可以用refresh_token來重新獲取token锈拨。

public async Task<string> GetOAuth2TokenByRefreshToken(string refreshToken)
{
    Uri uri = new Uri(baseUrl + oAuthTokenUrl);
    var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None, UseCookies = true };

    using (var client = new HttpClient(handler))
    {
        client.BaseAddress = uri;
        client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

        var content = new FormUrlEncodedContent(new Dictionary<string, string>()
        {
            {"grant_type", "refresh_token"},
            {"refresh_token", refreshToken},
            {"client_id", "app" },
            {"client_secret", "app"},
        });

        //獲取token保存到cookie,并設(shè)置token的過期日期                    
        var result = await client.PostAsync(uri, content);

        string tokenResult = await result.Content.ReadAsStringAsync();

        var tokenObj = (JObject)JsonConvert.DeserializeObject(tokenResult);
        string token = tokenObj["access_token"].ToString();
        string newRefreshToken = tokenObj["refresh_token"].ToString();
        long expires = Convert.ToInt64(tokenObj["expires_in"]);

        this.Response.SetCookie(new HttpCookie("access_token", token));
        this.Response.SetCookie(new HttpCookie("refresh_token", newRefreshToken));
        this.Response.Cookies["access_token"].Expires = Clock.Now.AddSeconds(expires);

        return tokenResult;
    }
}

這段代碼較直接使用用戶名密碼申請token的差別主要在參數(shù)上羹唠,{"grant_type", "refresh_token"},{"refresh_token", refreshToken}

4.4. 使用token訪問webapi

有了token佩微,訪問webapi就很簡單了缝彬。

public async Task<ActionResult> SendRequestWithOAuth2Token()
{
    var token = Request.Cookies["access_token"]?.Value;
    if (token == null)
    {
        //throw new Exception("token已過期");
        string refreshToken = Request.Cookies["refresh_token"].Value;
        var tokenResult = await GetOAuth2TokenByRefreshToken(refreshToken);
        var tokenObj = (JObject)JsonConvert.DeserializeObject(tokenResult);
        token = tokenObj["access_token"].ToString();
    }

    _abpWebApiClient.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token));

    return await GetUserList(baseUrl + webapiUrl);
}

這段代碼中,我們首先從cookie中取回access_token哺眯,若access_token為空說明token過期谷浅,我們就從cookie中取回refresh_token重新申請token。然后構(gòu)造一個Authorization將token信息添加到請求頭即可訪問目標(biāo)webapi。

5. 總結(jié)

本文介紹了三種不同的認(rèn)證方式進(jìn)行訪問webapi壳贪,并舉例說明陵珍。文章不可能面面俱到,省略了部分代碼违施,請直接參考源碼互纯。若有紕漏之處也歡迎大家留言指正。

本文主要參考自以下文章:
使用OAuth打造webapi認(rèn)證服務(wù)供自己的客戶端使用
ABP中使用OAuth2(Resource Owner Password Credentials Grant模式)
Token Based Authentication using ASP.NET Web API 2, Owin, and Identity

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末磕蒲,一起剝皮案震驚了整個濱河市留潦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辣往,老刑警劉巖兔院,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異站削,居然都是意外死亡坊萝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進(jìn)店門许起,熙熙樓的掌柜王于貴愁眉苦臉地迎上來十偶,“玉大人,你說我怎么就攤上這事园细〉牖” “怎么了?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵猛频,是天一觀的道長狮崩。 經(jīng)常有香客問我,道長鹿寻,這世上最難降的妖魔是什么睦柴? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮烈和,結(jié)果婚禮上爱只,老公的妹妹穿的比我還像新娘。我一直安慰自己招刹,他們只是感情好恬试,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疯暑,像睡著了一般训柴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妇拯,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天幻馁,我揣著相機(jī)與錄音洗鸵,去河邊找鬼。 笑死仗嗦,一個胖子當(dāng)著我的面吹牛膘滨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播稀拐,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼火邓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了德撬?” 一聲冷哼從身側(cè)響起铲咨,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜓洪,沒想到半個月后纤勒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡隆檀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年摇天,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刚操。...
    茶點(diǎn)故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡闸翅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菊霜,到底是詐尸還是另有隱情,我是刑警寧澤济赎,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布鉴逞,位于F島的核電站,受9級特大地震影響司训,放射性物質(zhì)發(fā)生泄漏构捡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一壳猜、第九天 我趴在偏房一處隱蔽的房頂上張望勾徽。 院中可真熱鬧,春花似錦统扳、人聲如沸喘帚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吹由。三九已至,卻和暖如春朱嘴,著一層夾襖步出監(jiān)牢的瞬間倾鲫,已是汗流浹背粗合。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留乌昔,地道東北人隙疚。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像磕道,于是被迫代替她去往敵國和親供屉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評論 2 361

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