iOS中OAuth2的運用之微信第三方登錄和獲取Github用戶信息

OAuth2簡介

閱讀時長45分鐘,Demo涉及iOS的SwiftUI和Swift,用到了原生ASWebAuthenticationSession類來進行授權(quán)服務锣光,涉及MVVM架構(gòu)踪危,并采用了原生的Combine響應式框架。

在傳統(tǒng)的開發(fā)中猪落,服務器(例如Github)給客戶端授權(quán)時(授權(quán)用戶登錄以及授權(quán)用戶獲取某些存在服務器上的資源)一般都是保存用戶名密碼贞远,就是我們登錄Github網(wǎng)站輸入的用戶名密碼;但假如有一個第三方App(咱們自己開發(fā)的移動端App)也需要訪問服務器(Github)的資源時笨忌,就會產(chǎn)生以下問題:

  1. 咱們自己開發(fā)的App得安全保存用戶的Github賬號和密碼蓝仲,這是很麻煩的事情。
  2. 假如咱們的App采用密碼與賬號登錄了Github官疲,萬一App被破解了或者說自己開發(fā)的登錄界面不安全袱结,這很可能導致用戶Github賬戶泄漏了。
  3. 服務器無法控制第三方App的權(quán)限途凫,因為使用密碼賬號登錄等價于是有獲取服務器所有資源的權(quán)限垢夹,一般資源提供方可不太樂意給這么大的權(quán)限。

以上問題如何解決呢维费,這時候OAuth就閃亮登場了果元,OAuth其實就是一套保證第三方App通過網(wǎng)絡服務能夠獲取資源所有者的資源的機制,看不懂這句話可以忽略犀盟,其實第三方App無需進行用戶驗證(就是所說的登錄)而晒,既然想獲取資源的服務器已經(jīng)有完整的安全的登錄機制(別人登錄的早寫好了,而且還安全)阅畴,這些事情交給他們做就好倡怎,畢竟咱們是大哥,這些小事不必親自出馬贱枣,比如:

  • 想利用微信進行第三方登錄监署,那就直接讓微信App完成登錄,然后讓微信告訴我們的App用戶已登錄冯事,至于怎么通知焦匈,后續(xù)會細說。
  • 想在咱們自己開發(fā)的App登錄Github昵仅,那就直接調(diào)用Github官方的登錄界面就好缓熟,用戶登錄完同樣讓Github通知咱們的App用戶已登錄,至于怎么通知摔笤,后續(xù)會細說够滑。

登錄完事后,要獲取資源咋辦吕世,這時候在Github登錄完后會給一個access_token到咱們的App彰触,這就是去拿資源的鑰匙當然這把鑰匙是有效期的而且還很短,其實一個access_token不夠命辖,因為不可能鑰匙失效了况毅,你就讓用戶再次登錄分蓖,這樣體驗感巨差,其實Github還給了一把refresh_token尔许,有效期稍微長點么鹤,顧名思義就是當access token失效時用這把refresh_token來獲取新的access_token,假如refresh_token也失效味廊,那對不起了蒸甜,趕緊讓用戶登錄來獲取新一輪的access_tokenrefresh_token

????怕你還是不明白(其實我想要點贊余佛!)柠新,咱再來取個粗暴的例子吧,就比如你現(xiàn)在住在了上海的湯臣一品高端小區(qū)內(nèi)(當然作為新生代農(nóng)民工的你我是不可能的)辉巡,小區(qū)的大門有一個超高的安保系統(tǒng)的恨憎,想進入小區(qū)得輸入房號密碼才能進入,假如要點外賣吃了(畢竟程序員是不做飯的)郊楣,美團騎手準時到達了你的小區(qū)大門框咙,然而你住32樓頂層,穿著大褲衩痢甘,不想下去迎接外賣小哥喇嘱,畢竟是高檔小區(qū)的住戶,畢竟是叫的上門服務塞栅,下去到小區(qū)大門開門迎接有失身份者铜,這時美團騎手瘋狂用過微信語音你,讓你告訴大門的房號密碼他好進去放椰,你愿意給嗎作烟?那我肯定不得給啊,我給了不就他天天可以來砾医?(這對于單身的你多危險呀)拿撩,就算我不點外賣他也能進來,這時候聰明的你決定給他授權(quán)如蚜,對沒錯是授權(quán)压恒,你在家遠程操控門禁系統(tǒng),別大驚小怪错邦,現(xiàn)在的小區(qū)都能遠程操控探赫,你輸入了你的房號密碼,然后生成了一個授權(quán)碼撬呢,比如5201314伦吠,但是這個授權(quán)碼是有時間的,你設置了只有10分鐘有效,美團騎手高興的輸入了你給的授權(quán)碼門開了毛仪,這樣你就吃上了美味(但愿)的外賣而不用下樓搁嗓,且不用擔心美團騎手能隨便進來,畢竟你只給授權(quán)碼設置了10分鐘的有效期箱靴。
????上面的例子中谱姓,騎手就好像是第三方APP,小區(qū)是某個擁有APP想要資源的服務器刨晴,騎手要進入小區(qū)不用輸入房號密碼,只需要得到服務器授權(quán)面的例子中路翻,騎手就好像是第三方APP狈癞,小區(qū)是某個擁有App想要資源的服務器妄均,騎手要進入小區(qū)不用輸入房號密碼还棱,待安保系統(tǒng)驗證后就頒發(fā)了5201314的授權(quán)碼。

例子也舉了爱榔,在得來一遍官方的定義讓你更加深刻掉冶,在OAuth一共有四個角色:

  • Resource Owner可以給資源是否能被獲取授權(quán)的 -- 小區(qū)大門門禁
  • Resource Server擁有資源的服務器 -- 小區(qū)
  • Client想去獲取資源的第三方APP -- 美團騎手
  • Authorization Server在驗證通過后頒發(fā)token的 -- 手機上生成token的軟件真竖,搞不好是個公眾號

讀到這想必你已了解了OAuth的大致原理,接下來我們進行二個實戰(zhàn)

  • 開發(fā)一個iOS App用來顯示Github用戶的遠程倉庫
  • 微信第三方登錄

Github App獲取遠程倉庫代碼傳送門

首先打開Github官網(wǎng)并進行登錄(需要翻墻)厌小,然后點擊賬戶找到Setting > Developer Setting > New Github App

image.png

然后在圖示的地方填上相應的信息

  • 在1處填上我們所創(chuàng)建App的名字恢共,這個會保存在Github上以便區(qū)分不同的App
  • 在2處填上App的描述信息
  • 在3處Homepage URL填上能鏈接到App主頁面的鏈接,這里沒有就隨便填百度
  • 在4處填上回調(diào)URL璧亚,這個有啥用待會解釋讨韭,可以按照圖中的填

我怕你不賴煩看到后面了,提前解釋吧癣蟋,callBackUrl是用來在請求用戶登錄的服務器返回的重定向URL透硝,簡單來說就是我這邊已確認登錄了,你想去到哪個頁面疯搅,這個就看咱們App邏輯怎么定濒生,網(wǎng)頁端是跳轉(zhuǎn)至重定向的網(wǎng)頁,而在移動端一般是回退到App界面幔欧,比如微信第三方登錄罪治,登陸完你得回到App呀,這里也一樣礁蔗,Github登錄完也需要回到App规阀,這個回調(diào)URL就是起這個作用。

  • 在5處把勾選上瘦麸,用于獲取refresh_token

image.png

接下來按照如下視圖填即可谁撼,注意Webhook的Avtive勾不用打上
image.png

接下來的Permissions Setting就是給資源授權(quán)的,為了方便起見,將Contents設置為Read-only
image.png

最后勾選Only on this account
image.png

點擊創(chuàng)建按鈕后厉碟,會來到如下的詳細頁面喊巍,點擊Generate a new client secret生成客戶端秘鑰,用來授權(quán)能使用Github的API箍鼓,這個界面請截圖保留崭参,Github不會讓你看到第二次
image.png

Xcode項目的創(chuàng)建

Demo采用swift和swiftUI進行創(chuàng)建,不熟悉的朋友可以看注釋款咖,只需要理解代碼的邏輯即可何暮,同時項目用到了ASWebAuthenticationSession這個類,這是Apple’s Authentication Services framework的一個API铐殃,用來給用戶通過網(wǎng)路服務進行授權(quán)海洼。

UI的搭建,主要代碼如下:

import SwiftUI

struct SignInView: View {
 // 綁定了SignInViewModel用于跳轉(zhuǎn)至個人倉庫列表頁
  @ObservedObject private var viewModel = SignInViewModel()

  var body: some View {
    NavigationView {
      VStack(spacing: 30) {
        NavigationLink(
          destination: RepositoriesView(displayed: $viewModel.isShowingRepositoriesView),
          isActive: $viewModel.isShowingRepositoriesView
        ) { EmptyView() }

        Image("rw-logo")
          .resizable()
          .aspectRatio(contentMode: .fit)
          .frame(width: 300, height: 200, alignment: .center)

        if viewModel.isLoading {
          ProgressView()
        } else {
          Button(action: { viewModel.signInTapped() }, label: {
            Text("Sign In")
              .font(Font.system(size: 24).weight(.semibold))
              .foregroundColor(Color("rw-light"))
              .padding(.horizontal, 50)
              .padding(.vertical, 8)
          })
          .background(buttonBackground)
        }
      }
      .navigationBarHidden(true)
      .onAppear {
        viewModel.appeared()// 請求登錄的用戶的相關(guān)信息
      }
    }
  }

  private var buttonBackground: some View {
    RoundedRectangle(cornerRadius: 8)
      .fill(Color("rw-green"))
  }
}

代碼解讀:

  • 通過綁定SignInViewModel來決定顯示登陸頁還是倉庫詳情頁富腊,綁定的操作需要了解Swift的原生響應式編程Combine框架
  • 通過判斷ViewModelisLoading屬性來是否展示Progress進度條
  • ViewModelisShowingRepositoriesView屬性為true時觸發(fā)NavigationLink跳轉(zhuǎn)至倉庫詳情頁
  • 在界面出現(xiàn)時請求登錄的用戶的相關(guān)信息坏逢,當然在沒登錄時打印錯誤信息

網(wǎng)絡請求

import Foundation

struct NetworkRequest {
  enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
  }

  enum RequestError: Error {
    case invalidResponse
    case networkCreationError
    case otherError
    case sessionExpired
  }

  enum RequestType: Equatable {
    case codeExchange(code: String)
    case getRepos
    case getUser
    case signIn

    func networkRequest() -> NetworkRequest? {
      guard let url = url() else {
        return nil
      }
      return NetworkRequest(method: httpMethod(), url: url)
    }

    private func httpMethod() -> NetworkRequest.HTTPMethod {
      switch self {
      case .codeExchange:
        return .post
      case .getRepos:
        return .get
      case .getUser:
        return .get
      case .signIn:
        return .get
      }
    }

    private func url() -> URL? {
      switch self {
      case .codeExchange(let code):
        let queryItems = [
          URLQueryItem(name: "client_id", value: NetworkRequest.clientID),
          URLQueryItem(name: "client_secret", value: NetworkRequest.clientSecret),
          URLQueryItem(name: "code", value: code)
        ]
        return urlComponents(host: "github.com", path: "/login/oauth/access_token", queryItems: queryItems).url
      case .getRepos:
        guard
          let username = NetworkRequest.username,
          !username.isEmpty
        else {
          return nil
        }
        return urlComponents(path: "/users/\(username)/repos", queryItems: nil).url
      case .getUser:
        return urlComponents(path: "/user", queryItems: nil).url
      case .signIn:
        let queryItems = [
          URLQueryItem(name: "client_id", value: NetworkRequest.clientID)
        ]

        return urlComponents(host: "github.com", path: "/login/oauth/authorize", queryItems: queryItems).url
      }
    }

    private func urlComponents(host: String = "api.github.com", path: String, queryItems: [URLQueryItem]?) -> URLComponents {
      switch self {
      default:
        var urlComponents = URLComponents()
        urlComponents.scheme = "https"
        urlComponents.host = host
        urlComponents.path = path
        urlComponents.queryItems = queryItems
        return urlComponents
      }
    }
  }

  typealias NetworkResult<T: Decodable> = (response: HTTPURLResponse, object: T)

  // MARK: Private Constants
  static let callbackURLScheme = "填入自己的"
  static let clientID = "填入自己的"
  static let clientSecret = "填入自己的"

  // MARK: Properties
  var method: HTTPMethod
  var url: URL

  // MARK: Static Methods
  static func signOut() {
    Self.accessToken = ""
    Self.refreshToken = ""
    Self.username = ""
  }

  // MARK: Methods
  func start<T: Decodable>(responseType: T.Type, completionHandler: @escaping ((Result<NetworkResult<T>, Error>) -> Void)) {
    var request = URLRequest(url: url)
    request.httpMethod = method.rawValue
    if let accessToken = NetworkRequest.accessToken {
      request.setValue("token \(accessToken)", forHTTPHeaderField: "Authorization")
    }
    let session = URLSession.shared.dataTask(with: request) { data, response, error in
      guard let response = response as? HTTPURLResponse else {
        DispatchQueue.main.async {
          completionHandler(.failure(RequestError.invalidResponse))
        }
        return
      }
      guard
        error == nil,
        let data = data
      else {
        DispatchQueue.main.async {
          let error = error ?? NetworkRequest.RequestError.otherError
          completionHandler(.failure(error))
        }
        return
      }
      // 1
      if T.self == String.self,
        let responseString = String(data: data, encoding: .utf8) {
        // 2 分割
        let components = responseString.components(separatedBy: "&")
        var dictionary: [String: String] = [:]
        // 3 提取
        for component in components {
          let itemComponents = component.components(separatedBy: "=")
          if let key = itemComponents.first,
             let value = itemComponents.last {
            dictionary[key] = value
          }
        }
        // 4 回到主線程刷新UI
        DispatchQueue.main.async {
          // 5 保存token
          NetworkRequest.accessToken = dictionary["access_token"]
          NetworkRequest.refreshToken = dictionary["refresh_token"]
          completionHandler(.success((response, "Success" as! T)))
        }
        return
      } else if let object = try? JSONDecoder().decode(T.self, from: data) {
        DispatchQueue.main.async {
          if let user = object as? User {
            NetworkRequest.username = user.login
          }
          completionHandler(.success((response, object)))
        }
        return
      } else {
        DispatchQueue.main.async {
          completionHandler(.failure(NetworkRequest.RequestError.otherError))
        }
      }
    }
    session.resume()
  }
}

對網(wǎng)絡的請求進行了簡單的封裝,由于需要在獲取資源的過程中有登錄Github賬戶赘被,請求token和請求資源的操作是整,所以采用了RequestType這個枚舉類型來定義網(wǎng)絡請求的類型,并根據(jù)httpMethod()方法來根據(jù)請求的類型來選用是Get還是Post請求民假,同時利用url()方法根據(jù)不同的請求類型生成相應的請求URL

代碼解讀

  • 請求URL的生成采用了urlComponents這個類浮入,沒用過的可自行了解,看一下代碼其實也能明白是啥回事
  • 由于請求token需要使用注冊所用的clientSecret羊异,clientIDcallbackURLScheme等舵盈,在 MARK: Private Constants處填入自己上面注冊的
  • 將返回的refresh_tokenaccess_token進行了本地持久化存儲(實際開發(fā)中最好使用保存在鑰匙串中的方式),二個token的作用上文已說明球化,用于在Github上作為令牌請求數(shù)據(jù)使用
  • 所有請求上送的參數(shù)需要去看Github官方的API接口文檔要求

請求個人Github遠端Repos

import SwiftUI

struct RepositoriesView: View {
  @ObservedObject private var viewModel = RepositoriesViewModel()
  @Binding private var displayed: Bool

  init(
    viewModel: RepositoriesViewModel = RepositoriesViewModel(),
    displayed: Binding<Bool>
  ) {
    self.viewModel = viewModel
    self._displayed = displayed
  }

  var body: some View {
    VStack {
      Text(viewModel.username)
        .font(.system(size: 20))
        .fontWeight(.semibold)
        .padding()
      List {
        ForEach(viewModel.repositories) { repo in
          Text(repo.name)
        }
      }
    }
    .navigationBarTitle("My Repositories", displayMode: .inline)
    .navigationBarItems(leading: signOutButton)
    .navigationBarBackButtonHidden(true)
    .onAppear {
      viewModel.load() // 界面出現(xiàn)時請求Repos
    }
  }

  private var signOutButton: some View {
    Button("Sign Out") {
      viewModel.signOut()
      displayed = false
    }
  }
}
import SwiftUI

class RepositoriesViewModel: ObservableObject {
  @Published private(set) var repositories: [Repository]
  let username: String

  init() {
    self.repositories = []
    self.username = NetworkRequest.username ?? ""
  }

  private init(
    repositories: [Repository],
    username: String
  ) {
    self.repositories = repositories
    self.username = username
  }

  func load() {
    NetworkRequest
      .RequestType
      .getRepos
      .networkRequest()?
      .start(responseType: [Repository].self) { [weak self] result in
        switch result {
        case .success(let networkResponse):
          DispatchQueue.main.async {
            self?.repositories = networkResponse.object
          }
        case .failure(let error):
          print("Failed to get the user's repositories: \(error)")
        }
      }
  }

  func signOut() {
    NetworkRequest.signOut()
  }

代碼解讀:

  • 請求Repos成功后設置RepositoriesViewModelrepositories屬性秽晚,用于刷新RepositoriesView的數(shù)據(jù)源
  • 退出登錄后觸發(fā)NetworkRequest.signOut()即清空accessTokenrefreshTokenusername

總結(jié):

  1. 進入登錄頁面時嘗試調(diào)用getUser方法請求用戶信息筒愚,由于第一次肯定沒有登錄赴蝇,會打印Failed to get user, or there is no valid/active session錯誤信息。
  2. 點擊SignIn登錄按鈕后巢掺,觸發(fā)NetworkRequestsignIn請求句伶,這時會調(diào)用原生的ASWebAuthenticationSession這個類,其實就是提供了一個Safari頁面展示Github的登錄界面陆淀,并負責拿到請求返回的結(jié)果考余,同時根據(jù)callbackURL來回到我們的App,將控制權(quán)交回 App轧苫。
  3. 登錄成功后拿到Code并利用此Code去請求access_tokenrefresh_token楚堤,并進行本地化保存
  4. 請求token成功后直接觸發(fā)getUser()請求用戶數(shù)據(jù),請求成功后設置SignInViewModelisShowingRepositoriesViewtrue,跳轉(zhuǎn)至RepositoriesView身冬。
  5. RepositoriesView開始顯示時觸發(fā)getRepos請求衅胀,展示個人Repo信息。

微信第三方登錄代碼傳送門

看完了上面的例子酥筝,再來看微信第三方登錄就非常簡單滚躯,由于篇幅有限,就只闡述實現(xiàn)的主要步驟嘿歌,并貼出主要代碼掸掏,具體實現(xiàn)看Demo,只要知道了OAuth2原理宙帝,這些套路都一樣

image.png

上面這個配方熟悉嗎丧凤,是不是一看就懂!Nice茄唐,記得點贊

微信開放平臺注冊App

Github一樣,微信也需要知道是誰要訪問它的資源蝇更,這需要到微信開放平臺注冊App(需要審核的)沪编,然后拿到appIDappSecret,這個簡單的小事就在此略過年扩。

集成微信登錄SDK

這個沒啥好說的蚁廓,支持手動集成和CocoaPods集成,具體集成看微信官方文檔微信SDK接入文檔

設置URLscheme

按照下圖設置URL Schemes厨幻,這是用來微信客戶端返回我們App用的相嵌,原則就是保住唯一性,可以把申請的appID填寫在這里

image.png

開始擼代碼

Demo采用了OC况脆,由于這里只是說明第三方微信登錄的實現(xiàn)過程饭宾,只貼出主要邏輯代碼


#import "AppDelegate.h"
#import "WXApi.h"
#import "ViewController.h"
@interface AppDelegate ()<WXApiDelegate>
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //appID,向微信注冊App
    [WXApi registerApp:appId];
    return YES;
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{
    return [WXApi handleOpenURL:url delegate:self];
}
// 微信的回調(diào)
- (void)onResp:(BaseResp *)resp{
    if([resp isKindOfClass:[SendAuthResp class]]){
        SendAuthResp *resp2 = (SendAuthResp *)resp;
        [[NSNotificationCenter defaultCenter] postNotificationName:@"wxLogin" object:resp2];
    }else{
        NSLog(@"授權(quán)失敗");
    }
}

代碼解讀:

  • 先向微信客戶端注冊App
  • 實現(xiàn)openURL方法格了,并設置代理
  • 在微信回調(diào)里拿到授權(quán)消息體
- (void)login{
    //判斷微信是否安裝
    if([WXApi isWXAppInstalled]){
        SendAuthReq *req = [[SendAuthReq alloc] init];
        req.scope = @"snsapi_userinfo";
        req.state = @"App";
        [WXApi sendAuthReq:req viewController:self delegate:self];
    }else{
        [self setupAlertController];
    }
}

代碼解讀:

  • 判斷是否安裝了微信客戶端
  • 安裝了則向微信客戶端發(fā)送登錄請求看铆,這會拉起微信客戶端,跳轉(zhuǎn)至微信客戶端
- (void)wxLogin:(NSNotification*)noti{
    //獲取到code
    SendAuthResp *resp = noti.object;
    NSLog(@"%@",resp.code);
    _code = resp.code;
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    NSString *url = [NSString stringWithFormat:@"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%@&secret=%@&code=%@&grant_type=%@",appId,appSecret,_code,@"authorization_code"];
    manager.requestSerializer = [AFJSONRequestSerializer serializer];
    [manager.requestSerializer setValue:@"text/html; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
    NSMutableSet *mgrSet = [NSMutableSet set];
    mgrSet.set = manager.responseSerializer.acceptableContentTypes;
    [mgrSet addObject:@"text/html"];
    //因為微信返回的參數(shù)是text/plain 必須加上 會進入fail方法
    [mgrSet addObject:@"text/plain"];
    [mgrSet addObject:@"application/json"];
    manager.responseSerializer.acceptableContentTypes = mgrSet;
    [manager GET:url parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {   
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"success");
        NSDictionary *resp = (NSDictionary*)responseObject;
        NSString *openid = resp[@"openid"];
        NSString *accessToken = resp[@"access_token"];
        NSString *refreshToken = resp[@"refresh_token"];
        if(accessToken && ![accessToken isEqualToString:@""] && openid && ![openid isEqualToString:@""]){
            [[NSUserDefaults standardUserDefaults] setObject:openid forKey:WX_OPEN_ID];
            [[NSUserDefaults standardUserDefaults] setObject:accessToken forKey:WX_ACCESS_TOKEN];
            [[NSUserDefaults standardUserDefaults] setObject:refreshToken forKey:WX_REFRESH_TOKEN];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
        [self getUserInfo];
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    }];
}

代碼解讀:

  • 登錄成功后拿到code盛末,然后利用codeappId弹惦,secret去請求access_tokenrefresh_token并進行保存。
  • 請求token成功后悄但,利用token去請求用戶數(shù)據(jù)棠隐,包括用戶微信名,圖像等等檐嚣,在實際開發(fā)中會將這些數(shù)據(jù)保存在后臺助泽。
  • access_token失效后利用refresh_token去重新請求的代碼這里沒貼出,具體看Demo

總結(jié):

本文通過二個實例Demo來說明了OAuth2的工作原理报咳,只要理解透了OAuth2的工作原理侠讯,類似于新浪,淘寶等第三方登錄實現(xiàn)都差不多暑刃,當然現(xiàn)在友盟可以一鍵集成這些登錄厢漩,但是內(nèi)部的實現(xiàn)原理還是逃不出OAuth2的,看到這里岩臣,希望大家能理解透OAuth2的作用溜嗜,最后希望大家能點贊一個,這是對我最大的鼓勵架谎,謝謝啦!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末炸宵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谷扣,更是在濱河造成了極大的恐慌土全,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件会涎,死亡現(xiàn)場離奇詭異裹匙,居然都是意外死亡,警方通過查閱死者的電腦和手機末秃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門概页,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人练慕,你說我怎么就攤上這事惰匙。” “怎么了铃将?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵项鬼,是天一觀的道長。 經(jīng)常有香客問我劲阎,道長秃臣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任哪工,我火速辦了婚禮奥此,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雁比。我一直安慰自己稚虎,他們只是感情好,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布偎捎。 她就那樣靜靜地躺著蠢终,像睡著了一般序攘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寻拂,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天程奠,我揣著相機與錄音,去河邊找鬼祭钉。 笑死瞄沙,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的慌核。 我是一名探鬼主播距境,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼垮卓!你這毒婦竟也來了垫桂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤粟按,失蹤者是張志新(化名)和其女友劉穎诬滩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灭将,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡疼鸟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了宗侦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愚臀。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡忆蚀,死狀恐怖矾利,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情馋袜,我是刑警寧澤男旗,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站欣鳖,受9級特大地震影響察皇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泽台,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一什荣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧怀酷,春花似錦稻爬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琉雳。三九已至,卻和暖如春友瘤,著一層夾襖步出監(jiān)牢的瞬間翠肘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工辫秧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留束倍,地道東北人。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓茶没,卻偏偏與公主長得像肌幽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抓半,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

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