使用 AFNetworking 自动刷新令牌的最佳解决方案?

Posted

技术标签:

【中文标题】使用 AFNetworking 自动刷新令牌的最佳解决方案?【英文标题】:Best solution to refresh token automatically with AFNetworking? 【发布时间】:2014-08-18 07:11:58 【问题描述】:

一旦您的用户登录,您将获得一个令牌(digestoauth),您将其设置到您的 HTTP 授权标头中,并授权您访问您的网络服务。 如果您将用户名、密码和此令牌存储在手机的某个位置(在用户默认值中,或者最好在钥匙串中),那么每次应用程序重新启动时,您的用户都会自动登录。

但是如果您的令牌过期怎么办?然后你“只需要”要求一个新的令牌,如果用户没有更改他的密码,那么他应该再次自动登录。

实现此令牌刷新操作的一种方法是继承 AFHTTPRequestOperation 并处理 401 Unauthorized HTTP 状态代码以请求新令牌。颁发新令牌后,您可以再次调用失败的操作,现在应该会成功。

那么您必须注册这个类,以便每个 AFNetworking 请求(getPath、postPath、...)现在都使用这个类。

[httpClient registerHTTPOperationClass:[RetryRequestOperation class]]

以下是此类的一个示例:

static NSInteger const kHTTPStatusCodeUnauthorized = 401;

@interface RetryRequestOperation ()
@property (nonatomic, assign) BOOL isRetrying;
@end

@implementation RetryRequestOperation
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *, id))success
                              failure:(void (^)(AFHTTPRequestOperation *, NSError *))failure

    __unsafe_unretained RetryRequestOperation *weakSelf = self;

    [super setCompletionBlockWithSuccess:success failure:^(AFHTTPRequestOperation *operation, NSError *error) 
        // In case of a 401 error, an authentification with email/password is tried just once to renew the token.
        // If it succeeds, then the opration is sent again.
        // If it fails, then the failure operation block is called.
        if(([operation.response statusCode] == kHTTPStatusCodeUnauthorized)
           && ![weakSelf isAuthenticateURL:operation.request.URL]
           && !weakSelf.isRetrying)
        
            NSString *email;
            NSString *password;

            email = [SessionManager currentUserEmail];
            password = [SessionManager currentUserPassword];
            // Trying to authenticate again before relaunching unauthorized request.
            [ServiceManager authenticateWithEmail:email password:password completion:^(NSError *logError) 
                if (logError == nil) 
                    RetryRequestOperation *retryOperation;

                    // We are now authenticated again, the same request can be launched again.
                    retryOperation = [operation copy];
                    // Tell this is a retry. This ensures not to retry indefinitely if there is still an unauthorized error.
                    retryOperation.isRetrying = YES;
                    [retryOperation setCompletionBlockWithSuccess:success failure:failure];
                    // Enqueue the operation.
                    [ServiceManager enqueueObjectRequestOperation:retryOperation];
                
                else
                
                    failure(operation, logError);
                    if([self httpCodeFromError:logError] == kHTTPStatusCodeUnauthorized)
                    
                        // The authentication returns also an unauthorized error, user really seems not to be authorized anymore.
                        // Maybe his password has changed?
                        // Then user is definitely logged out to be redirected to the login view.
                        [SessionManager logout];
                    
                
            ];
        
        else
        
            failure(operation, error);
        
    ];


- (BOOL)isAuthenticateURL:(NSURL *)url

    // The path depends on your implementation, can be "auth", "oauth/token", ...
    return [url.path hasSuffix:kAuthenticatePath];


- (NSInteger)httpCodeFromError:(NSError *)error

    // How you get the HTTP status code depends on your implementation.
    return error.userInfo[kHTTPStatusCodeKey];

请注意,此代码不能按原样工作,因为它依赖于外部代码,而这些代码取决于您的 Web API、授权类型(摘要、誓言等)以及您使用的框架类型通过 AFNetworking(例如 RestKit)。

这是非常有效的,并且已经证明使用与 CoreData 绑定的 RestKit 可以很好地处理摘要和 oauth 授权(在这种情况下,RetryRequestOperation 是 RKManagedObjectRequestOperation 的子类)。

我现在的问题是:这是刷新令牌的最佳方式吗? 我实际上想知道是否可以使用NSURLAuthenticationChallenge 以更优雅的方式解决这种情况。

【问题讨论】:

我同意 Wain 的观点,即这个实现更易于阅读和理解。不过,有一个小批评:AFNetworking 保证将调用成功或失败块。到达[SessionManager logout] 的代码路径违反了这个保证。 感谢@AaronBrager,关于您的简洁评论,示例代码现已修复。 伙计,我也在用RestKit+CoreData,我只想说,你的方法太对了。谢谢。 @Phil 你能告诉我如何使用方法覆盖创建子类的对象吗?我的意思是,如果我使用的是 RestKit,我会创建一个 RKManagedObjectRequestOperation- (id)appropriateObjectRequestOperationWithObject:(id)object,但我需要它作为我的子类的一个实例。所以在这种情况下,我无法实现要调用的重写方法。 @Phil 抱歉这个愚蠢的问题。我找到了一个方法- (BOOL)registerRequestOperationClass:(Class)operationClass。您的解决方案完美运行,再次感谢您。 【参考方案1】:

您当前的解决方案有效,并且您拥有它的代码,可能有合理数量的代码来实现它,但该方法有优点。

使用基于NSURLAuthenticationChallenge 的方法意味着在不同级别进行子类化,并使用setWillSendRequestForAuthenticationChallengeBlock: 扩充每个创建的操作。一般来说,这将是一种更好的方法,因为将使用单个操作来执行整个操作,而不必复制它并更新详细信息,并且操作身份验证支持将执行身份验证任务而不是操作完成处理程序。这应该是需要维护的代码更少,但该代码可能会被更少的人理解(或者大多数人需要更长的时间才能理解),因此事情的维护方面可能会总体上保持平衡。

如果您正在寻找优雅,那么请考虑进行更改,但鉴于您已经有了一个可行的解决方案,否则几乎没有什么收获。

【讨论】:

你能给出一些伪代码吗?我尝试调用[AFHTTPRequestOperation setWillSendRequestForAuthenticationChallengeBlock:],但尽管请求收到 401 状态代码,但从未调用该块。或者AFHTTPClient有什么具体的配置吗? 设置后会直接从connection:willSendRequestForAuthenticationChallenge:委托方法调用。【参考方案2】:

我正在寻找这个问题的答案,"Matt", the creator of AFNetworking, suggest this:

我发现处理这个问题的最佳解决方案是使用依赖 NSOperations 在任何之前检查一个有效的、未过期的令牌 允许传出请求通过。到时候,就看 开发人员确定刷新的最佳行动方案 令牌,或首先获得一个新令牌。

【讨论】:

以上是关于使用 AFNetworking 自动刷新令牌的最佳解决方案?的主要内容,如果未能解决你的问题,请参考以下文章

访问令牌和刷新令牌最佳实践?如何实现访问和刷新令牌

何时用刷新令牌交换访问令牌

在 nodejs 中使用 JWT 刷新令牌的最佳实践

在 .Net Core 中实现刷新令牌功能的最佳位置?

同时从移动设备和 Web 刷新 JWT 令牌的最佳实践

刷新 MongoDB 中的令牌