Token过期的处理方案
一、Token失效时间
如果App是新闻类、游戏类、聊天类等需要长时间的用户粘性,一般可以设置1年的有效时间
如果用户是支付类、银行类。一般token只得有小时间比较短15分钟左右。
二、Token失效处理方案
方案一:使用一个有效期很长的Token 机制
方案二:使用一个长期的Refresh Token 和 短期的Access Token.
考虑到这一点, Oauth 2.0 标准推荐采用Refresh Token和Access Token.
Refresh Token 有效期很长, Access Token 有效期很短.
用户登陆后,同时获得Refresh Token 和 Access Token,平时就用 Access Token, Access Token 过期后就用Refresh Token 获取新的Access Token.
这个方案使用很广泛,包括微信公众平台开发 也使用这个机制,但细细一想, 这个机制并不比方案二(使用一个长期的token)安全,黑客如果能够获取Access Token,获取Refresh Token也不难,采用两个token 仅仅是给黑客增加点小麻烦.
一旦黑客获取了获取Refresh Token, 就可反复的刷新的Access Token
方案三:Token以旧换新的机制
这个机制只使用一个短期的Token,比如1天.
用户登陆后, 这个Token发给客户端, 用户每次请求就使用这个Token认证身份, Token过期后凭此token换取新的Token,一个过期的Token只能换取一个新的Token,这是关键. 如果Token被盗, 黑客要持续使用也需持续的换取新的Token, 服务器一旦发现,一个旧Token多次试图换取新Token,表示有异常. 这时强制用户再次登陆.
Token旧换新,不一定等过期了才换,应用启动时就可旧换新,这个视具体情况而定.
总结:其实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
Was this helpful?