移動 App 的開發是基於現有的 Web 開發的基礎上產生的,因此網絡通訊通常都是基於 HTTP 協議通訊,而 HTTP 是一種無狀態協議,因此針對 HTTP 協議狀態保存一直都是永恆的話題。對於傳統 Web 開發來說,Cookie 和 Session 是最好的選擇,在最先的時候,只有 Cookie 一種方案,可是這種方案存在缺陷,也就是容易被修改,因此結合 Cookie 就提出了 Session 這種服務端存儲狀態的方法。json
可是移動 App 開發和傳統 Web 開發是存在區別的,相比 Web 開發被侷限於一個域中,移動 App 開發更加靈活,因此就須要更方便的機制用於受權認證。固然,並非說移動 App 開發作不到 Session 這種方式,只須要在 HTTP 部分填充服務端返回的 Cookie字段,天然就能作到 Session。後端
HTTP 現有不少種認證機制,原生 HTTP 就有 Basic Auth
、Digest
,在 HTTP 基礎上提出的認證也有不少,可是其中最知名最普遍的就是 OAuth2.0 認證。瀏覽器
引用百度百科上的定義:安全
OAuth 協議爲用戶資源的受權提供了一個安全的、開放而又簡易的標準。與以往的受權方式不一樣之處是 OAuth 的受權不會使第三方觸及到用戶的賬號信息(如用戶名與密碼),即第三方無需使用用戶的用戶名與密碼就能夠申請得到該用戶資源的受權,所以 OAuth 是安全的。OAuth 是 Open Authorization 的簡寫。服務器
OAuth 協議實際上不是一個專門爲了移動客戶端提出的協議,它的原本意義是隔離受權和認證,方便第三方應用存取資源,可是實際上因爲 OAuth 的便捷性,已經成爲實質上的移動客戶端認證方式。網絡
OAuth 有 1.0 和 2.0 兩個版本,實際內容差很少,2.0 版本是對 1.0 版本的擴充和修復,可是 2.0 版本不向下兼容 1.0 版本,因此目前使用的基本都是 2.0 版本。app
OAuth 自己不存在一個標準的實現,後端開發者本身根據實際的需求和標準的規定實現。其步驟通常以下:框架
客戶端要求用戶給予受權ide
用戶贊成給予受權函數
根據上一步得到的受權,向認證服務器請求令牌(token)
認證服務器對受權進行認證,確認無誤後發放令牌
客戶端使用令牌向資源服務器請求資源
資源服務器使用令牌向認證服務器確認令牌的正確性,確認無誤後提供資源
受權能夠是不一樣的內容和方式,OAuth2.0 定義了四種受權方式
受權碼模式
簡化模式
密碼模式
受權碼模式是目前功能最爲完備使用最普遍的 OAuth 認證方式,目前市場上大部分的針對第三方應用的開放平臺都是這種形式。阮一峯大神在本身的博客中已經有了不少講述,可是估計太過於深,因此不少人都是看的雲裏霧裏,這裏就拿一般狀況下的認證模式打比方。
對於客戶端來講,最終的要求就是訪問到資源服務器,而且從資源服務器獲取用戶的資源,可是資源服務器須要令牌(AccessToken),因此就須要向認證服務器得到令牌,因爲受權模式不容許客戶端代替用戶提交用戶名密碼,因此就須要使用連接跳轉到認證服務器的認證界面,可是,須要在 QueryString 附上 ClientID 和 RedirectUri,ClientID 用於標識客戶端,從認證服務器註冊後得到,RedirectUri 則是客戶端後臺服務器,而後用戶在認證服務器提供的頁面上填寫用戶名密碼。注意,這裏的頁面是認證服務器提供的,也就是說,客戶端無從插手用戶名密碼的輸入,這最大限度的保障了用戶名密碼的安全,而後認證服務器檢查用戶名密碼的正確性,若是正確,則跳轉到指定的 RedirectUri,而且在 QueryString 上附帶 AuthorizationCode,後臺服務器使用 AuthorizationCode 向認證服務器獲取 AccessToken,認證服務器則在 Response 域中返回 AccessToken,這樣就能夠訪問資源服務器了。
簡化模式和受權碼模式基本同樣,除了沒有客戶端的後臺服務器做爲中轉,而是直接在瀏覽器 Uri 中請求令牌,這裏就很少講,直接百度就行。
密碼模式是一種不多見又官方的的模式,它和客戶端模式是複用的,它不多在實際開放平臺中使用是由於用戶須要向客戶端提供用戶名和密碼,由客戶端向認證服務器得到 AccessToken,而後使用 AccessToken 向資源服務器請求資源,這種狀況實際上很是危險,由於客戶端能夠以明文的形式得到用戶名和密碼,因此在其餘狀況能使用的時候少用這種狀況。
這種狀況是目前大部分中小型公司在開發客戶端的時候使用最普遍的模式,嚴格來講,客戶端模式不屬於 OAuth2.0 規範須要解決的問題,而是一種從密碼模式演化而來的模式。它直接傳遞給認證服務器 ClientID,而後認證服務器返回 AccessToken。可是因爲大部分公司不須要向第三方應用開放接口,不須要創建開放平臺,在必定程度上是和密碼模式複用的。用戶在客戶端上註冊,認證服務器實際上就是後臺服務器,而後使用用戶名密碼返回 AccessToken。
在客戶端開發中,最多見的就是密碼模式,客戶端獲取用戶名密碼,向後臺服務器請求 AccessToken,使用 AccessToken 向後臺服務器其餘 API 接口請求數據。對於大部分開發者來講,都是本身實現具體的業務邏輯處理,包括筆者,可是後來筆者發現了 AFNetworking 團隊實際上已經本身提供了一套 OAuth2.0 認證機制模塊 AFOAuth2Manager,足以適用於大部分狀況了,因此這裏直接剖析其源碼,借鑑其精華。
AFOAuth2Manager 其實是依託於 AFNetworking 框架的一個擴展模塊,實際上代碼量很是小,就兩個模塊 AFOAuth2Manager
和 AFOAuthCredential
,前者包含了全部的網絡通訊代碼,後者則是存儲 AccessToken 的模型類,文檔介紹很是簡單,就介紹了密碼認證的流程
Authorizing Requests
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
模塊,裏面聲明瞭繼承自 NSObject
的 AFOAuthCredential
類和繼承自 AFHTTPRequestOperationManager
的 AFOAuth2Manager
,咱們新來看 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 框架的朋友應該不會陌生,這是一個網絡通訊類,裏面有三個成員變量 serviceProviderIdentifier
、clientID
、useHTTPBasicAuthentication
。
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 也已經失效,就須要使用存儲的用戶名密碼從新登陸,或者說不存儲用戶名密碼而是彈出登陸界面讓用戶自行填寫登陸。