Token过期的处理方案

一、Token失效时间

  • 如果App是新闻类、游戏类、聊天类等需要长时间的用户粘性,一般可以设置1年的有效时间

  • 如果用户是支付类、银行类。一般token只得有小时间比较短15分钟左右。

二、Token失效处理方案

  1. 方案一:使用一个有效期很长的Token 机制

  2. 方案二:使用一个长期的Refresh Token 和 短期的Access Token.

    1. 考虑到这一点, Oauth 2.0 标准推荐采用Refresh Token和Access Token.

    2. Refresh Token 有效期很长, Access Token 有效期很短.

    3. 用户登陆后,同时获得Refresh Token 和 Access Token,平时就用 Access Token, Access Token 过期后就用Refresh Token 获取新的Access Token.

    4. 这个方案使用很广泛,包括微信公众平台开发 也使用这个机制,但细细一想, 这个机制并不比方案二(使用一个长期的token)安全,黑客如果能够获取Access Token,获取Refresh Token也不难,采用两个token 仅仅是给黑客增加点小麻烦.

    5. 一旦黑客获取了获取Refresh Token, 就可反复的刷新的Access Token

  3. 方案三:Token以旧换新的机制

    1. 这个机制只使用一个短期的Token,比如1天.

    2. 用户登陆后, 这个Token发给客户端, 用户每次请求就使用这个Token认证身份, Token过期后凭此token换取新的Token,一个过期的Token只能换取一个新的Token,这是关键. 如果Token被盗, 黑客要持续使用也需持续的换取新的Token, 服务器一旦发现,一个旧Token多次试图换取新Token,表示有异常. 这时强制用户再次登陆.

    3. Token旧换新,不一定等过期了才换,应用启动时就可旧换新,这个视具体情况而定.

    4. 总结:其实token失效,自动刷新token,在页面只有一个请求的时候是比较好处理的,但是如果页面同时有多个请求,并且都会产生token失效,这就需要一些稍微复杂的处理,解决方式主要是用了Promise 函数来进行处理。每一个token失效的请求都会存到一个Promise函数集合里面,当刷新token的函数执行完毕后,才会批量执行这些Promise函数,返回请求结果。还有一点要注意一下,这儿设置一个刷新token的开关isRefreshing,这个是非常有必要的,防止重复请求。

三、Token失效案例

3.1 以设计招商银行的APP为例:

1, 采用https 加密,确保传输安全.

2,Token的有效期设为15分钟,Token每15分钟,以旧换新换取新的Token. 正常情况下,这个以旧换新对用户不可见,一但两人试图以旧换新,两人都阻止,需要再次登陆.

3,对于修改密码和转账支付这样的关键操作,要求用户输入密码.

3.2 无忧BB APP为例-旧Token换新Token

注意点:

  • 换Token的流程是,当发送请求是,发现Token失效,就会调用更新Token接口,用旧Token换新Token,如果成功返回,就将新的Token保存。这里面的关键点是:

  • 更换Token后,要重新请求刚才失败的请求,这里的难点是怎么拿到之前的请求,因为刷新Token的方法都是写在基类请求Block中,所以可以把当前的请求保存到一个单例对象中,等换到新的Token后,就从单例对象中取出之前的请求,重新发送一次,并且在Block中通过原始请求的Block回调返回结果。

  • 因为App使用时候存在多个请求同时发送的情况,这样会导致两个问题,一个是刷新Token的方法会被连续多次调用,这里面会存在问题;还有一个问题是需要把失败的请求都放到单例对象数组中,之后刷新到新的Token,重新发送之前所有失败的请求。

//
//  MXAPIYTRequest
//
//  Created by codeRiding on 2017/8/8.
//  Copyright © 2017年 codeRiding. All rights reserved.
//

#import "MXAPIYTRequest.h"
#import "MXBMode.h"
#import <AFNetworking/AFNetworking.h>
#import "MXAPIAFNRequest.h"
#import "MXAPIYTBatchRequest.h"

@implementation MXAPIYTRequest
@dynamic tag;

+ (MXAPIYTRequest *)lw_requestWithCompletion:(NetworkCompletion)completionBlock
{
    MXAPIYTRequest *r =  [[[self class] alloc] init];
    
    [r lw_requestWithCompletion:^(MXBMode *m)
     {
         if (completionBlock)
             completionBlock(m);
     }];
    
    return r;
}

- (void)lw_requestWithCompletion:(NetworkCompletion)completionBlock
{
    @weakify(self);
    [self startWithCompletionBlockWithSuccess:^(__kindof YTKBaseRequest * _Nonnull request){
        @strongify(self);
        if ([self isKindOfClass:[lw_api_userConfig class]]) {
            
        }else{
        
            WYLog(@"【YTNetwork-成功请求数据:】%@",request.description);
            WYLog(@"【YTNetwork-成功返回数据:】%@",request.responseObject);
        }
        
        MXBMode *m = [[MXBMode alloc] initModeWithDic:request.responseObject];
        
        m.sourceData = request.responseObject;
        if (completionBlock){
            completionBlock(m);
        }
        
    } failure:^(__kindof YTKBaseRequest * _Nonnull request) {
        @strongify(self);
        WYLog(@"【YTNetwork-失败请求数据:】%@",request.description);
        WYLog(@"【YTNetwork-失败请求数据:】%@",request.responseObject);
        
        MXBMode *m;
        
        if (request.responseObject) {
            m = [[MXBMode alloc] initModeWithDic:request.responseObject];
        } else{
            m = [[MXBMode alloc] init];
            m.code = CODE_REQUESTFAIL;
            m.message  = @"网络错误或者服务器错误";
            m.isSuccess = NO;
        }
        
        // 处理token过期
        if (m.code == CODE_UNVAILAGETOKEN || m.code == CODE_LOGIN_OTHER_PLACE)
        {
            [[MXAPIYTBatchRequest shareInstance] addRequest:self];
            
            if ([MXAPIAFNRequest shareInstance].dataTask) {
                [[MXAPIAFNRequest shareInstance].dataTask cancel];
            }
            
            [[MXAPIAFNRequest shareInstance] mx_afnRequestRefreshTokenCompleteBlock:^(id  _Nonnull responseObject) {
                @strongify(self);
                if (responseObject) {
                    MXBMode *responseMode = [MXBMode yy_modelWithDictionary:responseObject];
                    if (responseMode.isSuccess) {
                        lw_mode_exchangeToken *t = [lw_mode_exchangeToken yy_modelWithDictionary:responseMode.data];
                        
                        if (t.access_token.length > 0) {
                            [[MXDataHelper shareInstance] exchangeToken:t.access_token];
                            WYLog(@"失败的请求数组:%@",[MXAPIYTBatchRequest shareInstance].batchRequestArray);
                            
                            for (YTKRequest *rr in [MXAPIYTBatchRequest shareInstance].batchRequestArray) {
                                WYLog(@"重发请求:%@",rr);
                                [self.params setValue:[MXDataHelper shareInstance].userData.access_token forKey:@"access_token"];
                                [rr startWithCompletionBlockWithSuccess:^(__kindof YTKBaseRequest * _Nonnull request) {
                                    @strongify(self);
                                    WYLog(@"【YTNetwork-重发成功请求数据:】%@",request.description);
                                    WYLog(@"【YTNetwork-重发成功返回数据:】%@",request.responseObject);
                                    MXBMode *m = [[MXBMode alloc] initModeWithDic:request.responseObject];
                                    
                                    m.sourceData = request.responseObject;
                                    if (completionBlock){
                                        completionBlock(m);
                                    }
                                } failure:^(__kindof YTKBaseRequest * _Nonnull request) {
                                    MXBMode *m;
                                    
                                    if (request.responseObject) {
                                        m = [[MXBMode alloc] initModeWithDic:request.responseObject];
                                    } else{
                                        m = [[MXBMode alloc] init];
                                        m.code = CODE_REQUESTFAIL;
                                        m.message  = @"网络错误或者服务器错误";
                                        m.isSuccess = NO;
                                    }
                                    
                                    if (completionBlock) {
                                        completionBlock(m);
                                    }
                                    
                                    WYLog(@"【YTNetwork-重发数据失败:】%@",request.description);
                                    WYLog(@"【YTNetwork-重发数据失败:】%@",request.responseObject);
                                }];
                            }
                            
                            [[MXAPIYTBatchRequest shareInstance].batchRequestArray removeAllObjects];
                            
                        }else{
                            [[MXDataHelper shareInstance] handleLogoutData];
                            [[NSNotificationCenter defaultCenter] postNotificationName:@"xbbRelogin" object:nil];
                        }
                    }else{
                        [[MXDataHelper shareInstance] handleLogoutData];
                        [[NSNotificationCenter defaultCenter] postNotificationName:@"xbbRelogin" object:nil];
                    }
                }
            }];
        }else{
            if (completionBlock) {
                completionBlock(m);
            }
        }
        
    }];
}


/**
 设置请求头部

 @return 数据
 */
- (NSDictionary<NSString *,NSString *> *)requestHeaderFieldValueDictionary{
    
    /*刘汶
    if (CurrentUser.getToken){
        
        NSMutableDictionary *dict = @{}.mutableCopy;
        if (CurrentUser.getToken) {
            dict[@"token"] = CurrentUser.getToken;
        }
        if (CurrentUser.getUserData.madminId) {
            dict[@"lgId"] = CurrentUser.getUserData.madminId;
        }
        return dict;
    }else{
        return @{};
    }
     */
    
    return @{};
}


/**
 子类可以重写改方法

 @return 设置请求类型
 */
- (YTKRequestMethod)requestMethod
{
    return YTKRequestMethodPOST;
}

/**
  子类重写改方法

 @return 传入请求参数
 */
- (id)requestArgument
{
    if (!_params) {
        _params = [NSMutableDictionary dictionary];
    }
    
    if (_page) {
        [_params setValue:@"20" forKey:@"page_size"];
        [_params setValue:[NSString stringWithFormat:@"%d",_page] forKey:@"page"];
    }
    
    [_params setValue:[MXDataHelper shareInstance].userData.access_token forKey:@"access_token"];
    return _params;
}

@end

3.3 触点Life APP为例

//
//  HTTPRequest.h
//  TrudianLife
//
//  Created by CodeRiding on 2016/12/22.
//  Copyright © 2016年 trudian. All rights reserved.
//

#import "HTTPRequest.h"
#import "TDSaveUserData.h"
#import "TDNormalDataUtil.h"

@implementation HTTPRequest
static AFHTTPSessionManager *manager;

+ (AFHTTPSessionManager *)sharedHTTPSession
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [AFHTTPSessionManager manager];
        manager.responseSerializer = [AFJSONResponseSerializer serializerWithReadingOptions:NSJSONReadingAllowFragments];
        [manager.requestSerializer willChangeValueForKey:@"timeoutInterval"];
        [manager.requestSerializer didChangeValueForKey:@"timeoutInterval"];
        manager.requestSerializer.timeoutInterval = 8.0f;
        //如果报接受类型不一致请替换一致text/html或别的
        manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    });
    return manager;
}

#pragma mark - 网络请求
+ (void)POST:(NSString *)url body:(id)formData response:(HttpReqestBlock)block
{
    
    NSLog(@"[请求数据]%@\n%@",url,formData);
    
    AFHTTPSessionManager *manager = [self sharedHTTPSession];
    
    [manager POST:url parameters:formData progress:^(NSProgress * _Nonnull uploadProgress) {
        
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        NSLog(@"[返回数据_%@]\n%@",url,responseObject);
        
        TDResponse *value = [TDResponse mj_objectWithKeyValues:responseObject];
        
        if(value.rcode == 30061)
        {
            // rcode为30061表示需要重置rcode
            // 测试机和正式机互相置换token就会失效
            [TDNormalDataUtil requestResetBoToken:^(NSError *error, id response)
            {
                TDResponse *value = [TDResponse mj_objectWithKeyValues:response];
                if (value.rcode ==10000)
                {
                    NSDictionary *dict = [response dictionaryForKey:@"data"];
                    NSString *token = [dict stringForKey:@"token"];
                    if (token)
                    {
                        [[NSUserDefaults standardUserDefaults] setObject:token forKey:[TDSysConst tokenKey]];
                        [[NSUserDefaults standardUserDefaults] synchronize];
                        
                        // 更新用户信息
                        {
                            [TDNormalDataUtil requestBoDetail:^(NSError *error, id response)
                             {
                                 TDResponse *value = [TDResponse mj_objectWithKeyValues:response];
                                 if (value.rcode ==10000)
                                 {
                                     [TDSaveUserData handleBODetailResponse:response];
                                     
                                     // 重新请求失败的数据
                                     NSMutableDictionary* tempDic  = [formData mutableCopy];
                                     tempDic[@"key"] = token;
                                     [manager POST:url parameters:tempDic progress:^(NSProgress * _Nonnull uploadProgress) {
                                         
                                     } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
                                     {
                                         block(nil,responseObject);
                                     } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                                         
                                     }];
                                 }
                             }];
                            
                        }
                        
                        
                    }

                }

            }];
   
        }else{
            
            block(nil,responseObject);
        }

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        
        block(error,nil);
    }];
    
}

+ (void)POSTProgress:(NSString *)url body:(NSDictionary *)formData response:(ProgressBlock)block
{
    AFHTTPSessionManager *manager = [self sharedHTTPSession];
    [manager POST:url parameters:formData progress:^(NSProgress * _Nonnull uploadProgress) {
        block(nil,nil,uploadProgress);
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        block(nil,responseObject,@"100");
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"Error:%@",error);
        block(error,nil,@"0");
    }];
}

+ (void)GET:(NSString *)url query:(NSDictionary *)queryData response:(HttpReqestBlock)block{
    AFHTTPSessionManager *manager = [self sharedHTTPSession];
    [manager GET:url parameters:queryData progress:^(NSProgress * _Nonnull downloadProgress) {
        
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
         block(nil,responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"Error:%@",error);
        block(error,nil);
    }];

}

+ (void)DELETE:(NSString *)url body:(NSDictionary *)formData response:(HttpReqestBlock)block{
    AFHTTPSessionManager *manager = [self sharedHTTPSession];
    [manager DELETE:url parameters:formData success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
         block(nil,responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"Error:%@",error);
        block(error,nil);
    }];
  
}

@end

四、拓展

4.1 多台设备同时登录,设备的唯一性登录

  • 如果允许多台设备同时登录,并且可以设置最大登录的数量的时候。例如QQ:允许在电脑端登录,QQ手机端登录,QQ网页端登录

  • 如果超出这三个端,想要在另外一个相同的端登录,需要使对应的端token失效,来保证一个端一个账号只能登录一次。

  • 可以设置多个token根据登录端不同,来监测token是否过期。根据登录的数量可以判断最大支持多少个设备同时登录。

Last updated