# Token过期的处理方案

## 一、Token失效时间

* 如果App是新闻类、游戏类、聊天类等需要长时间的用户粘性，一般可以设置1年的有效时间
* 如果用户是支付类、银行类。一般token只得有小时间比较短15分钟左右。

## 二、Token失效处理方案

1. 方案一：使用一个有效期很长的Token 机制
2. 方案二：[使用一个长期的Refresh Token 和 短期的Access Token.](http://sevennight.cc/2016/07/19/auto_login_impl.html)
   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网页端登录&#x20;
* 如果超出这三个端,想要在另外一个相同的端登录，需要使对应的端token失效，来保证一个端一个账号只能登录一次。&#x20;
* 可以设置多个token根据登录端不同，来监测token是否过期。根据登录的数量可以判断最大支持多少个设备同时登录。
