OAuth2.0認證 iOS AFOAuth2Manager介紹

文章參考來自互聯網,原文鏈接文章最后有,歡迎到原文閱讀

前言: 關于實際應用問題,可以參考這篇文章基于AFNetworking 的OAuth2認證 類 AFOAuth2Manager

歷史

移動 App 的開發是基于現有的 Web 開發的基礎上產生的,所以網絡通信一般都是基于 HTTP 協議通信,而 HTTP 是一種無狀態協議,所以針對 HTTP 協議狀態保存一直都是永恒的話題。對于傳統 Web 開發來講,Cookie 和 Session 是最好的選擇,在最早的時候,只有 Cookie 一種方案,但是這種方案存在缺陷,也就是容易被修改,所以結合 Cookie 就提出了 Session 這種服務端存儲狀態的方法。

但是移動 App 開發和傳統 Web 開發是存在區別的,相比 Web 開發被局限于一個域中,移動 App 開發更加靈活,所以就需要更方便的機制用于授權認證。當然,并不是說移動 App 開發做不到 Session 這種方式,只需要在 HTTP 部分填充服務端返回的 Cookie字段,自然就能做到 Session。

HTTP 現有很多種認證機制,原生 HTTP 就有 Basic AuthDigest,在 HTTP 基礎上提出的認證也有很多,但是其中最知名最廣泛的就是 OAuth2.0 認證。

OAuth 歷史

引用百度百科上的定義:

OAuth 協議為用戶資源的授權提供了一個安全的、開放而又簡易的標準。與以往的授權方式不同之處是 OAuth 的授權不會使第三方觸及到用戶的帳號信息(如用戶名與密碼),即第三方無需使用用戶的用戶名與密碼就可以申請獲得該用戶資源的授權,因此 OAuth 是安全的。OAuth 是 Open Authorization 的簡寫。
OAuth 協議實際上不是一個專門為了移動客戶端提出的協議,它的本來意義是隔離授權和認證,方便第三方應用存取資源,但是實際上由于 OAuth 的便捷性,已經成為實質上的移動客戶端認證方式。

OAuth 有 1.0 和 2.0 兩個版本,實際內容差不多,2.0 版本是對 1.0 版本的擴充和修復,但是 2.0 版本不向下兼容 1.0 版本,所以目前使用的基本都是 2.0 版本。

OAuth 本身不存在一個標準的實現,后端開發者自己根據實際的需求和標準的規定實現。其步驟一般如下:

  1. 客戶端要求用戶給予授權
  2. 用戶同意給予授權
  3. 根據上一步獲得的授權,向認證服務器請求令牌(token)
  4. 認證服務器對授權進行認證,確認無誤后發放令牌
  5. 客戶端使用令牌向資源服務器請求資源
  6. 資源服務器使用令牌向認證服務器確認令牌的正確性,確認無誤后提供資源

授權可以是不同的內容和方式,OAuth2.0 定義了四種授權方式

  1. 授權碼模式
  2. 簡化模式
  3. 密碼模式
  4. 客戶端 OAuth 實踐

授權碼模式

授權碼模式是目前功能最為完備使用最廣泛的 OAuth 認證方式,目前市場上大部分的針對第三方應用的開放平臺都是這種形式。阮一峰大神在自己的博客中已經有了很多講述,但是估計太過于深,所以很多人都是看的云里霧里,這里就拿通常情況下的認證模式打比方。

對于客戶端來說,最終的要求就是訪問到資源服務器,并且從資源服務器獲取用戶的資源,但是資源服務器需要令牌(AccessToken),所以就需要向認證服務器獲得令牌,由于授權模式不允許客戶端代替用戶提交用戶名密碼,所以就需要使用鏈接跳轉到認證服務器的認證界面,但是,需要在 QueryString 附上 ClientID 和 RedirectUri,ClientID 用于標識客戶端,從認證服務器注冊后獲得,RedirectUri 則是客戶端后臺服務器,然后用戶在認證服務器提供的頁面上填寫用戶名密碼。注意,這里的頁面是認證服務器提供的,也就是說,客戶端無從插手用戶名密碼的輸入,這最大限度的保障了用戶名密碼的安全,然后認證服務器檢查用戶名密碼的正確性,如果正確,則跳轉到指定的 RedirectUri,并且在 QueryString 上附帶 AuthorizationCode,后臺服務器使用 AuthorizationCode 向認證服務器獲取 AccessToken,認證服務器則在 Response 域中返回 AccessToken,這樣就可以訪問資源服務器了。

簡化模式

簡化模式和授權碼模式基本一樣,除了沒有客戶端的后臺服務器作為中轉,而是直接在瀏覽器 Uri 中請求令牌,這里就不多講,直接百度就行。

密碼模式

密碼模式是一種很少見又官方的的模式,它和客戶端模式是復用的,它很少在實際開放平臺中使用是因為用戶需要向客戶端提供用戶名和密碼,由客戶端向認證服務器獲得 AccessToken,然后使用 AccessToken 向資源服務器請求資源,這種情況實際上非常危險,因為客戶端可以以明文的形式獲得用戶名和密碼,所以在其他情況能使用的時候少用這種情況。

客戶端模式

這種情況是目前大部分中小型公司在開發客戶端的時候使用最廣泛的模式,嚴格來說,客戶端模式不屬于 OAuth2.0 規范需要解決的問題,而是一種從密碼模式演化而來的模式。它直接傳遞給認證服務器 ClientID,然后認證服務器返回 AccessToken。但是由于大部分公司不需要向第三方應用開放接口,不需要建立開放平臺,在一定程度上是和密碼模式復用的。用戶在客戶端上注冊,認證服務器實際上就是后臺服務器,然后使用用戶名密碼返回 AccessToken。

客戶端的 OAuth 實踐

在客戶端開發中,最常見的就是密碼模式,客戶端獲取用戶名密碼,向后臺服務器請求 AccessToken,使用 AccessToken 向后臺服務器其他 API 接口請求數據。對于大部分開發者來說,都是自己實現具體的業務邏輯處理,包括筆者,但是后來筆者發現了 AFNetworking 團隊實際上已經自己提供了一套 OAuth2.0 認證機制模塊 AFOAuth2Manager,足以適用于大部分情況了,所以這里直接剖析其源碼,借鑒其精華。

內部模塊剖析

文件結構

AFOAuth2Manager 實際上是依托于 AFNetworking 框架的一個擴展模塊,實際上代碼量非常小,就兩個模塊 AFOAuth2ManagerAFOAuthCredential,前者包含了所有的網絡通信代碼,后者則是存儲 AccessToken 的模型類,文檔介紹非常簡單,就介紹了密碼認證的流程

Authentication 身份驗證
NSURL *baseURL = [NSURL URLWithString:@"http://example.com/"];
AFOAuth2Manager *OAuth2Manager =
            [[AFOAuth2Manager alloc] initWithBaseURL:baseURL
                                            clientID:kClientID
                                              secret:kClientSecret];

[OAuth2Manager authenticateUsingOAuthWithURLString:@"/oauth/token"
                                          username:@"username"
                                          password:@"password"
                                             scope:@"email"
                                           success:^(AFOAuthCredential *credential) {
                                               NSLog(@"Token: %@", credential.accessToken);
                                           }
                                           failure:^(NSError *error) {
                                               NSLog(@"Error: %@", error);
                                           }];
                                           
Authorizing Requests 授權請求
AFHTTPRequestOperationManager *manager =
    [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL];

[manager.requestSerializer setAuthorizationHeaderFieldWithCredential:credential];

[manager GET:@"/path/to/protected/resource"
  parameters:nil
     success:^(AFHTTPRequestOperation *operation, id responseObject) {
         NSLog(@"Success: %@", responseObject);
     }
     failure:^(AFHTTPRequestOperation *operation, NSError *error) {
         NSLog(@"Failure: %@", error);
     }];
Storing Credentials
[AFOAuthCredential storeCredential:credential
                    withIdentifier:serviceProviderIdentifier];
Retrieving Credentials
AFOAuthCredential *credential =
        [AFOAuthCredential retrieveCredentialWithIdentifier:serviceProviderIdentifier];

總共四個方法就概括了所有的 OAuth2.0 密碼認證流程。而模塊實際山也就只有4個文件,兩個頭文件兩個實現文件。
我們先來看 AFHTTPRequestSerializer+OAuth2 模塊,這個模塊實際上是 AFNetworking 其中 AFHTTPRequestSerializer 類的分類擴展,里面就聲明了一個方法

- (void)setAuthorizationHeaderFieldWithCredential:(AFOAuthCredential *)credential;

它的實現如下

- (void)setAuthorizationHeaderFieldWithCredential:(AFOAuthCredential *)credential {
    if ([credential.tokenType compare:@"Bearer" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
        [self setValue:[NSString stringWithFormat:@"Bearer %@", credential.accessToken] forHTTPHeaderField:@"Authorization"];
    }
}

這個方法使用傳入的 credential 參數,取出其中的 accessToken 成員,并且和 Bearer 字符串組合在一起,填充到 HTTP 的 Authorization 字段,這個字段是 OAuth2.0 規范規定的,當然,很多情況下我們可能不是傳遞 Bearer 字符串而是其他,完全可以新增一個方法。

再來看 AFOAuth2Manager 模塊,里面聲明了繼承自 NSObjectAFOAuthCredential 類和繼承自 AFHTTPRequestOperationManagerAFOAuth2Manager,我們新來看 AFOAuthCredential 類

@interface AFOAuthCredential : NSObject <NSCoding>

@property (readonly, nonatomic, copy) NSString *accessToken;
@property (readonly, nonatomic, copy) NSString *tokenType;
@property (readonly, nonatomic, copy) NSString *refreshToken;
@property (readonly, nonatomic, assign, getter = isExpired) BOOL expired;

+ (instancetype)credentialWithOAuthToken:(NSString *)token
                               tokenType:(NSString *)type;

- (id)initWithOAuthToken:(NSString *)token
               tokenType:(NSString *)type;

- (void)setRefreshToken:(NSString *)refreshToken;

- (void)setExpiration:(NSDate *)expiration;

- (void)setRefreshToken:(NSString *)refreshToken
             expiration:(NSDate *)expiration;

+ (BOOL)storeCredential:(AFOAuthCredential *)credential
         withIdentifier:(NSString *)identifier;

+ (BOOL)storeCredential:(AFOAuthCredential *)credential
         withIdentifier:(NSString *)identifier
      withAccessibility:(id)securityAccessibility;

+ (AFOAuthCredential *)retrieveCredentialWithIdentifier:(NSString *)identifier;

+ (BOOL)deleteCredentialWithIdentifier:(NSString *)identifier;

@end

這個類實現了 NSCoding 協議,用于持久化,并且有4個成員變量,用于存儲 accessToken、令牌類型、refreshToken 和 過期標志,基本沒什么要講的,不過我們在查看源碼的時候能發現以下內容

+ (BOOL)storeCredential:(AFOAuthCredential *)credential
         withIdentifier:(NSString *)identifier
      withAccessibility:(id)securityAccessibility
{
    NSMutableDictionary *queryDictionary = [AFKeychainQueryDictionaryWithIdentifier(identifier) mutableCopy];

很明顯,模塊使用鑰匙串來存儲憑證,但是實際上鑰匙串不能濫用,做過開發的朋友應該知道,用戶無法自行存取鑰匙串,應用程序才能使用鑰匙串,但是鑰匙串不像 NSUserDefault,應用程序卸載的時候鑰匙串內容是不會消失的,很容易導致鑰匙串內遺留垃圾數據,所以這里不應當使用自帶方法存儲,可以使用擴展自行實現 NSUserDefault 存儲憑證。

- (BOOL)isExpired {
    return [self.expiration compare:[NSDate date]] == NSOrderedAscending;
}

這里用 Swift 的話來說就是一個計算變量。通過比較過期日期和當前日期來確定是否過期,非常簡單的小技巧。
再來看最后一個 AFOAuth2Manager 模塊

@interface AFOAuth2Manager : AFHTTPRequestOperationManager

@property (readonly, nonatomic, copy) NSString *serviceProviderIdentifier;
@property (readonly, nonatomic, copy) NSString *clientID;
@property (nonatomic, assign) BOOL useHTTPBasicAuthentication;

+ (instancetype)clientWithBaseURL:(NSURL *)url
                         clientID:(NSString *)clientID
                           secret:(NSString *)secret;

- (id)initWithBaseURL:(NSURL *)url
             clientID:(NSString *)clientID
               secret:(NSString *)secret;

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                                   username:(NSString *)username
                                   password:(NSString *)password
                                      scope:(NSString *)scope
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure;

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                                      scope:(NSString *)scope
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure;

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                               refreshToken:(NSString *)refreshToken
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure;

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                                       code:(NSString *)code
                                redirectURI:(NSString *)uri
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure;

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                                 parameters:(NSDictionary *)parameters
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure;

@end

可以看到,這個類是繼承自 AFHTTPRequestOperationManager,使用過 AFNetworking 框架的朋友應該不會陌生,這是一個網絡通信類,里面有三個成員變量 serviceProviderIdentifierclientIDuseHTTPBasicAuthentication

serviceProviderIdentifier 是用于存儲和獲取 OAuth 憑證的標識符,clientID 就是客戶端ID,用于認證服務器標志客戶端。最后一個就是是否將 AccessToken 存放在 Authorization 字段,默認為 YES。

所有的初始化函數最終會使用 AFHTTPRequestOperationManager 的初始化函數使用 url 初始化整個網絡框架類,然后將 OAuth 認證信息傳遞給內部成員,最終代碼如下

- (id)initWithBaseURL:(NSURL *)url
             clientID:(NSString *)clientID
               secret:(NSString *)secret
{
    NSParameterAssert(clientID);

    self = [super initWithBaseURL:url];
    if (!self) {
        return nil;
    }
    
    self.serviceProviderIdentifier = [self.baseURL host];
    self.clientID = clientID;
    self.secret = secret;

    self.useHTTPBasicAuthentication = YES;

    [self.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Accept"];
    
    return self;
}

可以看到,實際上默認 useHTTPBasicAuthentication 為 YES,并且在 HTTP 頭字段添加了 application/json=Accept 鍵值對,表示接受 json 返回。

除了兩個初始化函數以外,還有5個請求函數

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
username:(NSString *)username
password:(NSString *)password
scope:(NSString *)scope 
success:(void ( ^ )( AFOAuthCredential *credential ))success 
failure:(void ( ^ ) ( NSError *error ))failure

這個函數很好理解,就是使用用戶名和密碼,并且以指定的 scope 請求 AccessToken。當然 scope 參數也可能是不存在的,因為很多后臺不需要這個參數。實際上最終這個函數是根據 OAuth2.0 規范,將 grant_type、username、password、scope 四個參數打包成字典然后傳遞給

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
parameters:(NSDictionary *)parameters 
success:(void ( ^ ) ( AFOAuthCredential *credential ))success 
failure:(void ( ^ ) ( NSError *error ))failure

方法。除此之外

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                                      scope:(NSString *)scope
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure
- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                               refreshToken:(NSString *)refreshToken
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure
- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                                       code:(NSString *)code
                                redirectURI:(NSString *)uri
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure

三個函數也是將其打包成字典然后傳遞給最后的方法,其中第三個函數就是 OAuth 授權碼模式的實現。
最后來看最終通信邏輯實現函數

- (AFHTTPRequestOperation *)authenticateUsingOAuthWithURLString:(NSString *)URLString
                                 parameters:(NSDictionary *)parameters
                                    success:(void (^)(AFOAuthCredential *credential))success
                                    failure:(void (^)(NSError *error))failure
{
    NSMutableDictionary *mutableParameters = [NSMutableDictionary dictionaryWithDictionary:parameters];
    if (!self.useHTTPBasicAuthentication) {
        mutableParameters[@"client_id"] = self.clientID;
        mutableParameters[@"client_secret"] = self.secret;
    }
    parameters = [NSDictionary dictionaryWithDictionary:mutableParameters];

    AFHTTPRequestOperation *requestOperation = [self POST:URLString parameters:parameters 
success:^(__unused AFHTTPRequestOperation *operation, id responseObject) {
        if (!responseObject) {
            if (failure) {
                failure(nil);
            }

            return;
        }

        if ([responseObject valueForKey:@"error"]) {
            if (failure) {
                failure(AFErrorFromRFC6749Section5_2Error(responseObject));
            }

            return;
        }

        NSString *refreshToken = [responseObject valueForKey:@"refresh_token"];
        if (!refreshToken || [refreshToken isEqual:[NSNull null]]) {
            refreshToken = [parameters valueForKey:@"refresh_token"];
        }

        AFOAuthCredential *credential = 
        [AFOAuthCredential credentialWithOAuthToken:[responseObject valueForKey:@"access_token"] 
tokenType:[responseObject valueForKey:@"token_type"]];


        if (refreshToken) { // refreshToken is optional in the OAuth2 spec
            [credential setRefreshToken:refreshToken];
        }

        // Expiration is optional, but recommended in the OAuth2 spec. 
        // It not provide, assume distantFuture === never expires
        NSDate *expireDate = [NSDate distantFuture];
        id expiresIn = [responseObject valueForKey:@"expires_in"];
        if (expiresIn && ![expiresIn isEqual:[NSNull null]]) {
            expireDate = [NSDate dateWithTimeIntervalSinceNow:[expiresIn doubleValue]];
        }

        if (expireDate) {
            [credential setExpiration:expireDate];
        }

        if (success) {
            success(credential);
        }
    } failure:^(__unused AFHTTPRequestOperation *operation, NSError *error) {
        if (failure) {
            failure(error);
        }
    }];
    
    return requestOperation;
}

其中主要是使用了 AFHTTPRequestOperation,最終返回也是這個對象,里面使用 block 包含了具體成功和失敗的邏輯過程,包括了拆包然后提取 refresh_token 等參數,需要注意的是在這個函數中,實際上已經調用了

AFOAuthCredential *credential = 
[AFOAuthCredential credentialWithOAuthToken:[responseObject valueForKey:@"access_token"] 
tokenType:[responseObject valueForKey:@"token_type"]];

代碼,也就是說,不需要開發者自己再手動將 accessToken 存儲到鑰匙串中。而開發者需要做的事情就是在所有的網絡通信之前使用

[manager.requestSerializer setAuthorizationHeaderFieldWithCredential:credential];

將憑證嵌入到 HTTP 頭中。

刷新憑證
OAuth1.0 規范中,允許 AccessToken 存在很長時間,或者是 RefreshToken 存在無限長時間,但是在 OAuth2.0 規范中就行不通了,這就需要使用 RefreshToken 刷新憑證,OAuth2.0 規范規定返回 AccessToken 的時候必須制定一個過期時間,一般是一個以秒為單位的時間長度,框架使用 expireDate = [NSDate dateWithTimeIntervalSinceNow:[expiresIn doubleValue]]; 將其轉換為 NSDate 類型存儲,一般來說,可以使用 isExpired 函數判斷是否已經過期,但是非常遺憾的是,很多情況下,后臺服務器過期時間根本就是瞎編的,所以也需要注意在過期時間之前,AccessToken 已經過期了的情況,一旦出現過期或者說沒有過期但是請求 API 接口返回 AccessToken 已經過期的情況,就需要使用 RefreshToken 刷新憑證,而 RefreshToken 實際上也是有一個過期日期的,但是這個過期日期規范并沒有規定后臺必須返回,所以就需要自行判斷后臺返回值,如果 RefreshToken 也已經失效,就需要使用存儲的用戶名密碼重新登錄,或者說不存儲用戶名密碼而是彈出登錄界面讓用戶自行填寫登錄。

文章來源: OAuth2.0認證
原文作者: 山河永寂

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容

  • 以下是官網直譯:https://oauth.net/ 1. 首頁 OAuth是一種開放協議(注:協議是公開的,任何...
    JacoChan閱讀 11,286評論 0 20
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,826評論 18 139
  • 什么是三方授權? 第三方授權就是,委托第三方來對既定的用戶進行鑒定,鑒定成功之后,下發信任憑證,信任憑證和用戶掛鉤...
    一只小哈閱讀 32,664評論 2 21
  • 文/洛夕璇 昨天從西寧飛往西安的飛機上,郭姐問我,你現在最大的煩惱是什么?我思考了很久,還是搖搖頭說,好像沒有。沉...
    洛夕璇閱讀 1,263評論 41 21
  • 今天看了《媽媽教的數學》講了關于粗心和引導孩子思考兩個章節。 只要媽媽留心自己的孩子,當發現他出現錯誤、失誤、挫折...
    葉子的愛閱讀 208評論 0 1