OAuth2.0认证

历史

移动 App 的开发是基于现有的 Web 开发的基础上产生的,因此网络通讯通常都是基于 HTTP 协议通讯,而 HTTP 是一种无状态协议,因此针对 HTTP 协议状态保存一直都是永恒的话题。对于传统 Web 开发来说,Cookie 和 Session 是最好的选择,在最先的时候,只有 Cookie 一种方案,可是这种方案存在缺陷,也就是容易被修改,因此结合 Cookie 就提出了 Session 这种服务端存储状态的方法。json

可是移动 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 版本。app

OAuth 自己不存在一个标准的实现,后端开发者本身根据实际的需求和标准的规定实现。其步骤通常以下:框架

  1. 客户端要求用户给予受权ide

  2. 用户赞成给予受权函数

  3. 根据上一步得到的受权,向认证服务器请求令牌(token)

  4. 认证服务器对受权进行认证,确认无误后发放令牌

  5. 客户端使用令牌向资源服务器请求资源

  6. 资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源

受权能够是不一样的内容和方式,OAuth2.0 定义了四种受权方式

  • 受权码模式

  • 简化模式

  • 密码模式

受权码模式

受权码模式是目前功能最为完备使用最普遍的 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 的模型类,文档介绍很是简单,就介绍了密码认证的流程

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 模块,里面声明了继承自 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 也已经失效,就须要使用存储的用户名密码从新登陆,或者说不存储用户名密码而是弹出登陆界面让用户自行填写登陆。

相关文章
相关标签/搜索