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

10

一旦您的用户登陆成功,你将得到一个 token (digestoauth),并设置在您的 HTTP Authorization header 中,这将授权您访问您的 Web 服务。

如果您将用户的用户名、密码和此 token 存储在手机上(在用户默认设置或者最好是在 keychain 中),那么每次应用程序重新启动时,用户都会自动登陆。

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

实现这个 token 刷新操作的一种方法是,子类化AFHTTPRequestOperation并处理401未经授权的HTTP状态码,以便请求一个新的 token。当新的 token 发布时,您可以再次调用失败的操作,这应该会成功。

然后您必须注册此类,使每个 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、授权方式(摘要、Oauth 等)以及您在 AFNetworking 上使用的框架类型(例如 RestKit)。

这种方法非常有效,并且已经被证明可以与使用 RestKit 绑定到 CoreData 的摘要和 Oauth 授权一起很好地工作(在这种情况下,RetryRequestOperation 是 RKManagedObjectRequestOperation 的子类)。

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


2
我同意Wain的观点,这个实现更易于阅读和理解。不过有一个小批评:AFNetworking保证要么调用成功块,要么调用失败块。到达[SessionManager logout]的代码路径违反了这个保证。 - Aaron Brager
感谢@AaronBrager,根据您的精彩评论,示例代码现已修复。 - Phil
兄弟,我也在使用RestKit+CoreData,我想说的是,你的方法非常正确。谢谢。 - kokoko
@Phil,你能告诉我如何使用方法覆盖创建子类的对象吗?我的意思是,如果我正在使用RestKit,我会使用- (id)appropriateObjectRequestOperationWithObject:(id)object创建一个RKManagedObjectRequestOperation,但我需要它成为我的子类的实例。所以在这种情况下,我无法调用我的重写方法。 - kokoko
@Phil,抱歉问了个愚蠢的问题。我找到了一个方法- (BOOL)registerRequestOperationClass:(Class)operationClass。你的解决方案完美地解决了我的问题,再次感谢你。 - kokoko
2个回答

1
你目前的解决方案可以工作,你已经有了代码,实现它可能需要相当多的代码,但这种方法有其优点。
使用基于 NSURLAuthenticationChallenge 的方法意味着在不同的层次上进行子类化,并通过 setWillSendRequestForAuthenticationChallengeBlock: 增强每个创建的操作。一般来说,这将是更好的方法,因为单个操作将用于执行整个操作,而不必复制它并更新细节,操作 auth 支持将执行 auth 任务,而不是操作完成处理程序。这应该是更少的代码来维护,但那些代码可能会被理解得更少(或者大多数人理解起来需要更长时间),所以事情的维护方面可能总体平衡。
如果你正在寻求优雅的方式,那么考虑改变,但是考虑到你已经有一个工作的解决方案,否则几乎没有什么收益。

1
你能提供一些伪代码吗?我尝试调用[AFHTTPRequestOperation setWillSendRequestForAuthenticationChallengeBlock:],但是即使请求收到401状态码,该块也从未被调用。或者在AFHTTPClient上需要进行特定的配置吗? - Phil
当设置后,它将直接从connection:willSendRequestForAuthenticationChallenge:委托方法中调用。 - Wain

0

我正在寻找这个问题的答案,"AFNetworking 的创始人 Matt 建议如下

我发现处理这个问题的最佳解决方案是使用依赖 NSOperations 来检查有效且未过期的令牌,然后才允许任何传出请求。此时,开发人员需要确定刷新令牌或首先获取新令牌的最佳操作方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接