关于request.HTTPBody一点思考
大情景
1 公司最近准备上线银行存管,相关的对接都已完成,现在进入测试尾声,准备近期发布新版上线。
2 存管相关目前都是通过加载h5,主要包括银行开户,充值,提现,投标等大功能模块。(细节很多,就不在这里说了)
3 对h5结果处理大概逻辑:存管页面成功之后会返回一个结果url,客户端截取到这个url,再做相应的处理,例如:投资成功就跳转到投资成功结果展示页,充值成功就跳转到充值结果倒计时页面等等(看需求,具体问题再具体处理)
4 基于之前需要更改uiwebview中ajax请求,程序中使用了NSURLProtocol(这是个抽象类,不可以被实例化)的子类,结果url的拦截就是在该类的canonicalRequestForRequest的方法进行的。
问题
当用户点击投标,跳转到投标存管页面输入交易密码等一系列操作完成之后,测试发现不管是投资成功还是投资失败(交易密码错误次数过多,账户被冻结),存管发的结果链接都是一样的,也就是说不管是投资成功还是失败,只要用户结束h5页面的操作,回到客户端本地时都是投资成功的页面,这显然是不对的。
解决方案
和领导沟通之后,决定如下解决:
步骤一,客户端拦截获取request.HTTPBody,从而截取结果url的tm和data值;
步骤二,请求接口将tm和data发送给后台;
步骤三,后台解密tm和data的到操作状态(成功或者失败),再返回给客户端
步骤四,客户端获得后台结果,成功则到本地投资成功页,失败则到申购页并toast 投资失败
意外情况:
1 h5投标页,当交易密码错误次数过多账号被冻结时,存管那边tm和data返回空
准备当tm和data为空的时候,客户端判断为冻结情况,但是发现出来 2 意外情况
2 发现程序能拦截到2次结果url,第一次有值,第二次没值
处理过程:
步骤一, Charles抓包发现,h5确实只发了一个请求(提醒同事是不是NSURLProtocol的问题?)
步骤二, 同事尝试使用uiwebview的shouldStartLoadWithRequest代理来截取结果url,发现真的只发了一次请求,确实是NSURLProtocol的问题
最终解决
在uiwebview的shouldStartLoadWithRequest中拦截结果url,截取request.HTTPBody,从而得到tm和data值,发送通知相应页面处理。充值模块大概代码如下(具体需求具体分析,我们项目大概逻辑是这样的):
//处理充值完成结果(成功和失败返回的url一样问题)
if ([request.URL.absoluteString rangeOfString:DepositeCharge].length > 0)
{
NSString *HTTPBodyString = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
NSMutableDictionary *payResultInfo = [NSMutableDictionary dictionaryWithCapacity:1];
[payResultInfo setObject:HTTPBodyString forKey:@"HTTPBodyString"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"DepositRechargeResultNotification" object:nil userInfo:payResultInfo];
}
相应页面处理逻辑:
//监听充值返回结果通知,放在viewdidload里面
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doResult:) name:@"DepositRechargeResultNotification" object:nil];
- (void)doResult:(NSNotification *)sender{
NSDictionary *dic = sender.userInfo;
NSString *httpBodyString = [dic objectForKey:@"HTTPBodyString"];
if (!(httpBodyString.length>0)) {
//交易密码错误累计5次账号被冻结之后,返回的url的httpBody tm和data都为空,此时客户端做的处理
[self.navigationController popToRootViewControllerAnimated:NO];
return;
}
else{
[[PersonalCenterLogicManager sharedInstance] reqResultInfoWithHttpBody:httpBodyString Success:^(id model) {
NSLog( @" 充值结果返回%@",model);
NSDictionary *dataDic= [model objectForKey:@"data"];
if ([[dataDic objectForKey:@"status" ]isEqualToString:@"S"]) {
//充值成功跳到充值过渡页
[self.navigationController pushViewController:[[ChargeSuccessVC alloc] initWithTitle:@"充值结果"] animated:NO];
}
else {
//失败回到充值页面
[self.navigationController popViewControllerAnimated:NO];
[MBProgressHUD showError:[dataDic objectForKey:@"msg"]];
}
} failure:^(NSError *err) {
[MBProgressHUD showError:ServerError];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.navigationController popToRootViewControllerAnimated:NO];
});
}];
}
}
编码问题
上面说到客户端截取结果url,之后获取tm和data值发送给后台,
开始我们是如下得到其值,之后解析并获取相应的tm和data值,AFN以post方式将两个参数传到后台,发现不行
NSString *HTTPBodyString = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
问题:
1 后台接收到值,无法解析(后台接收的值和客户端发送的值是否一致?若一致,后台解析问题或者压根这样的值是无法解析的;若不一致,说明这个请求的方式是否有问题?AFN post请求过程中 一些字符被自动转义了?。同事开发,具体情况不是很清楚,猜测应该是这样。)
2 程序里如何获取Charles抓包时Contents里面Form格式的数据,如下图:
而不是Text格式或者Raw格式(这些格式下面会发现有%2F %2B %3D %2B等转义字符),上述代码得到就是这种格式。如下:
网上搜了很多,最终如下获取:
NSString *HTTPBodyString = [[[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
3 最后同事那边跟后台调试之后,是以afn post方式,参数追加到url后面,真正参数为nil发送请求,ok。(有点乱,待调)
总结
不管什么问题,最终总有解决的办法(不管是去解决这个问题,还是通过别的方案去绕开这个问题),而我们需要的是静下心来分析,尽量从多个方面去分析(例如上面说到的程序截取到2次请求问题,也许我们会去让后台找原因,让他们不要发两次请求,当然这也是在解决问题。但同时我们也要想,是不是客户端这边的问题,是不是拦截的框架自己有问题呢,所以通过抓包,我们最后发现,后台确实只发了一次请求,是客户端截取url的框架自身问题。那个编码问题也是如此)
附:
以下是截取子串常用方法,先记录在这里,备以后需要,哈哈
NSString *strBody = [[NSString alloc]initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
NSLog(@"request.allHTTPbody = %@",strBody);
if ([request.URL.absoluteString rangeOfString:DepositRecharge].length > 0)
{
NSRange rangeAnd = [strBody rangeOfString:@"&"];
if (rangeAnd.length > 0) {
//substringFromIndex:截取从location开始到结束,包括location位置上的字符
NSString *strData = [strBody substringFromIndex:rangeAnd.location+1];
//substringToIndex:截取开始到location,不包括loacation位置上的字符
NSString *strTm = [strBody substringToIndex:rangeAnd.location];
NSLog(@"tm = %@",[strTm substringWithRange:NSMakeRange(3, strTm.length-3)]);
NSLog(@"data = %@",[strData substringWithRange:NSMakeRange(5, strData.length-5)]);
}
}
后续问题记录
NSURLProtocol h5 post请求变成get请求问题
具体问题:h5页面发送post请求,抓包确实也是post请求 但经过客户端之后就变成了get请求。
客户端对h5的处理:
基于有的活动h5页面,需要判断用户登录状态 从而显示相应的信息。此时h5链接会含有某个标识字符串(例如:auth 字符串),当客户端截取到该字段,会拦截该请求 添加token信息,之后再重新发送该请求。(基于webview不能拦截所有的请求,故项目使用NSURLProtocol子类来拦截请求)
最终解决:
通过不断的调试和分析,发现是因为之前的处理是 不管是get请求还是post请求,token信息一律都是追加到url后面。 我们只要对get和post做分别处理,get请求追加在url后,post请求追加在body里面,如此处理即可。代码如下:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
NSString *strURL = request.URL.absoluteString;
//h5 ajax请求中含authentication,截取token信息后重定向
if (([strURL rangeOfString:@"auth"].length > 0) &&
([request.HTTPMethod rangeOfString:@"POST"].length > 0)) {
NSString *strBody = [[NSMutableString alloc]initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
[mutableReqeust setHTTPBody: [[StaticTools addParaWithOriginUrl:strBody] dataUsingEncoding: NSUTF8StringEncoding]];
}
if (([strURL rangeOfString:@"auth"].length > 0) &&
([request.HTTPMethod rangeOfString:@"GET"].length > 0))
{
NSString *str = [StaticTools addParaWithOriginUrl:strURL];
mutableReqeust = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:str]];
}
return mutableReqeust;
}