文章參考來自互聯網,原文鏈接文章最后有,歡迎到原文閱讀
前言: 關于實際應用問題,可以參考這篇文章基于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
Auth
、Digest
,在 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 本身不存在一個標準的實現,后端開發者自己根據實際的需求和標準的規定實現。其步驟一般如下:
- 客戶端要求用戶給予授權
- 用戶同意給予授權
- 根據上一步獲得的授權,向認證服務器請求令牌(token)
- 認證服務器對授權進行認證,確認無誤后發放令牌
- 客戶端使用令牌向資源服務器請求資源
- 資源服務器使用令牌向認證服務器確認令牌的正確性,確認無誤后提供資源
授權可以是不同的內容和方式,OAuth2.0 定義了四種授權方式
- 授權碼模式
- 簡化模式
- 密碼模式
- 客戶端 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 框架的一個擴展模塊,實際上代碼量非常小,就兩個模塊 AFOAuth2Manager
和 AFOAuthCredential
,前者包含了所有的網絡通信代碼,后者則是存儲 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
模塊,里面聲明了繼承自 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 也已經失效,就需要使用存儲的用戶名密碼重新登錄,或者說不存儲用戶名密碼而是彈出登錄界面讓用戶自行填寫登錄。
文章來源: OAuth2.0認證
原文作者: 山河永寂