聊聊微信内页面发Ajax请求会重复请求的问题

本文揭示了在微信环境中使用AJAX请求时遇到的一个奇怪问题:同一请求被发送两次,导致后端处理逻辑异常。通过对请求过程的深入分析,最终发现这一现象与微信内置浏览器的特殊请求转发机制有关。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载于:https://blog.fishlee.net/2016/07/07/double-request-from-wechat-client-with-ajax/

背景

这是个大坑,耗费了我极多的时间。

事情呢,是这样的。最近几天做了一个微信里的潜入页,用于注册账户的。注册很简单,输入手机号-验证短信验证码-填一点资料-注册成功。

作为一个单页面操作,所有请求都是通过AJAX和服务器交互的,这思路很常规。唯一的特点是,最后一步超长。

超长的原因是:创建账户需要创建几百张表,还有无数初始化操作,所以乐观估计需要至少八秒钟才会成功。

也许你会问为什么会这么慢,要创建这么多表呢?

00089.gif

原谅我不想说,因为与本文无关。

00100.gif

然则这个创建其实是有步骤的,第一步就是把用户的邮箱给占位:创建为最基本的信息,不允许重复创建

OK,背景说完。

直到上线

开发,测试,包括内测都是很完美的,没有任何问题。

但上线后,突然有测试提出了这么一个问题:微信里注册的时候,任何一个邮箱都会提示邮箱已使用

面对这样一个错误且不管怎么换邮箱都同样的错误,所有人的表情都是懵逼的。

00244.gif

内心OS:这特么什么鬼。

后端(Java,没错,语法和性能巨烂的Java):这是个很低级的BUG,很明显是服务器首次返回错误信息后乃们直接缓存了记录信息,所以后面其实都没验证。

我还没去反驳,测试直接打脸。

测试:我第一次输入就这样。
Java:……

Java同学陷入了沉思。
反正我带着『不是自己问题』的乐观心态,坐在旁边看他们三脸懵逼。
后来Java同学很不乐意地回去看了下数据库,发现邮箱确实存在了。
Java同学一拍大腿,说哎呀你们这群测试都是猪呀,这明显存在嘛!

测试的同学一脸懵逼,极不情愿地又用了一个小号邮箱重新注册,一样的错误。
Java同学一查数据库,“行不行啊你们,这邮箱还是有啊”。
测试同学一脸茫然,说我准备重新申请个QQ,你丫别吵吵……你看看XXX这个邮箱存不存在。
Java:这个邮箱不存在。
测试同学低头鼓捣了一下,说,“一样的错误”。
Java同学低头查了一下,哎哟卧槽,怎么数据库有记录了,不是返回已存在了么。

然后所有人都沉默了一下,最后他们把头一起转向我,表情是这样的。

00160.gif

他们:一定是你重复发请求却只拿最后一次请求当结果了。
我直接甩锅:别看我,关我毛事,所有浏览器都测试过,按钮在发请求的时候都禁用了不可能重复点击,发请求状态都有标记不可能重复发,怎么可能发两次,你们别闹了,证据呢就乱说话。

Java同学回去给代码加上Log输出,然后一边测一边看输出。

然后他们看到了对我很不利的结果:

“木鱼你看,你就是发出了两次请求嘛!”

我看了看记录,特么的果然前后差了两秒钟,有两次开始执行记录的请求……

我……

00163.gif

漫长地找原因

我极不情愿地回来找前端的问题。

然而我不管怎么看代码,怎么调试,怎么用自己的微信测试,不管是安卓(小米)还是iPhone5S,都测不出半点问题。从程序逻辑上看也根本没有发两次请求的机会。

好郁闷……用了MVVM框架,难道这框架有问题??

00130.gif

然后我就开始怀疑是服务器那边的问题。Java的后端是跑在Tomcat上的,前面还有个Nginx做反代。于是我到运维那边找Ngxin的访问日志看了一下。

查看了前后关联的请求,确实有重复的,但是客户端IP都不一样时间也不一样,而同一个IP的请求前后都是一个序列不存在重复

所以根据Nginx的日志,很明显客户端没有重复请求啊,要是重复请求了,应该看到同一个IP的请求会重复出现才对。那么Java那边的重复记录怎么回事,难道Java这边自己调用了两次??

看着Java同学那么天真的脸,我觉得把这个锅就这样甩给他们是莫大的伤害。

于是我本着“有则改进无则加勉”的出发点,决定改前端代码。

第一次尝试,在进入ajax方法之前,加一个bool变量标记,进入时加标记,完成后清除标记,进入之前判断是否已标记,如果已标记则直接退出。
完全没改进,依然同样的错误。

第二次尝试,将Ajax方法改成同步的,我直接阻止你浏览器操作,不能重复操作总不会因为DOM事件重复发了吧?
完全没改进,依然同样的错误。

第三次尝试,挂载全局的Ajax钩子,在ajax完成后打印返回结果到dom里。
打印信息没重复,表明只收到了一个结果,那就是邮箱已存在。

第四次尝试,直接上jQuery的AjaxFilter,将并行的重复ajax请求,直接截断。
完全没结果。

第五次尝试,在Ajax之前,设置了一个alert弹窗警告。
神奇的,重复请求没了……

00136.gif

这特么都是什么跟什么啊。

这个测试花了我很久,因为每次修改需要我提交,然后java那边打包,然后运维那边上测试环境,流程很漫长。

此时,已经从晚上八点折腾到了十点多。

为了尽快弄明白问题出在哪里,决定抓包测试。拿出自己的手机,用微信访问页面注册,完全没有任何问题。

难道这和手机有关系?那出现这问题的手机也不是一部啊?拿了一部之前一直出问题的手机过来,连wifi设代理用Fiddler抓包。

完全正常,没有任何问题,注册流程很正常。

但是取消代理,就又只会报邮箱已存在的错误。

What the f*ck……

此时,已经是十一点多了。

这时候,他们提出了一个很有建设性(才怪)的建议,就说是不是因为那个alert导致请求延迟了几秒才正常的,或者这是jQuery的问题?

我很不情愿(并觉得他们纯粹是扯淡)地加了一个setTimeout测试。

涛声依旧啊  00494.gif 

到底咋回事?

根据Java的输出,是有返回创建成功的消息的。

然后我将所有的Ajax结果全部显示到DOM里,发现只有错误信息,却没有那个成功的信息。换句话说,如果请求确实重复发了,那么唯一能解释的是,js运行出错导致对应的消息没处理。

然而新版本的安卓版微信自带浏览器内核,不是系统的webview,所以要调试只能用微信自己的工具。不过好歹可以测试。

连上调试器,打断点,发现ajax函数只调用一次,没问题,但是唯一收到的消息就是返回了错误,却没有正常的结果。所以不是出错,而是确实就没有返回那个结果

看到这样的结果,所有人暴躁了,这特么到底什么事啊。

本来想在微信里抓包,然而微信调试工具里要抓包同样需要设代理,从之前测试的结果看,设代理就无此问题,又一次被卡住了。

此刻,我的内心是崩溃的,难道真的只能洗洗睡了吗。

00139.gif

啊,看到了曙光

此时,已经凌晨十二点多。为了尽快找到问题,后端的同学开始直接连上服务器实时输出Nginx的访问日志。

神奇的,点击一次注册,滚动出了两条日志……(原谅我没截图)……我一眼看过去,哎卧槽这不是我之前看到的那俩IP么,咋这时候还在???

看到的两条日志,除了客户端IP不一样之外,其它信息一模一样,包括地址、方法和UserAgent。客户端IP分别是123.151.42.57211.102.210.254211.102.210.254这个没啥问题,是这边的出口IP,那前面一个 123.151.42.57 是什么鬼?在ip138上查了一下,这是个天津的电信IP。然后这个IP的请求是先发出来的,比后面的请求提前了两秒钟,然而响应状态码是499,后面的是200

499状态码是什么错误呢?搜索了一下Nginx的错误码,指出这个(非标)错误是指客户端关闭了连接。

……………………客户端关闭了连接????

而邮箱已存在的错误,正是后面的那一条200请求返回的。

PHP的同学一拍大腿,卧槽这不是说超时了么,你丫去把请求的超时设长一点。

我不想跟他说话并想甩他一脸翔。

『我之前为了避免此问题已经将超时设成5分钟了。』

00288.gif

不过此时,我开始关注那俩IP了……

猜测

事已至此,我唯一的猜测就是,微信的浏览器里发请求,并不是一定直接向服务器发送请求的,而是通过了某些特定的中转服务器进行转发。至于转发的原因,可能是为了重排版(将PC的网页压缩重排版以节约手机流量或适应大小)或加速(压缩)抑或安全检测(中转的时候拦截已知恶意网址)。往前面翻了一下,前面的几步流程都是123.151.42.57这个IP转发的,而最后一个注册请求发出来了却在2秒后又由211.102.210.254直接的连接发起……我只能解释为这个中转服务器的超时时间是两秒钟,为了防止服务器网络问题导致用户过长等待,默认如果请求2秒钟没返回则放弃中转改为直接发起。

如果事实真如此,那么就能解释为什么前后会跟着两个请求,而前一个请求却在2秒钟后出499错误的情况了。
那么为什么用代理的时候没有此问题?很容易解释,如果系统已经设置为通过代理服务器访问了,那么软件会直接假定无法直接联网或无法简单可靠地通过反代访问,所以会放弃中转。

从前后间隔2秒的时间判断,大概超时时间就是2秒左右。那么可以做出假设,假定请求2秒钟内就会返回,则不会引发第二次请求。

Java同学改了一下接口,先不做具体操作直接返回成功,测试了一下,果然就不会有此问题顺利走通流程。

这个坑也是……活活把人坑到了凌晨1点。

00742.gif

后来作为改进的创建方案,Java端在确定可以创建后,直接返回成功,然后开后台任务异步创建。
没有去校验创建是否成功,因为他们觉得失败概率很低,就算失败了其实也没有啥可重试的方法,还是先不检查算了。

到此为止,问题顺利解决。

重现

现在问题解决了,但真正的问题还没解决:那就是我需要确定自己的结论。

根据上面的判断,有这样的结论:

  1. 微信里直接发出ajax请求的话,其实是通过一些特定的服务器中转的
  2. 这个中转不是全部的,和手机以及系统有关系(因为我的安卓手机微信就没有这现象,而有此情况的手机也不是某个特定的品牌,iPhone也没此问题)
  3. 这个中转的超时时间很短,一旦超时,会迅速回滚为非中转模式请求

但是这个模式有一个很致命的假定:就是它假定了所有的ajax请求都可以安全如此快速重试的,而没有考虑到关键请求处理时间较长且无法重复发送的情况(比如此例)。

为了确定是否有此问题,我解析了一个测试域名 debug.fishlee.net,并模拟了一个带有长、短请求的测试页面,并开启跟踪。短请求即时返回,长请求则会阻断当前的操作10秒钟再结束。

测试代码如下。

<%@ Page Language="C#" AutoEventWireup="true" %>
 
<script type="text/c#" runat="server">
 
    void Page_Load(object sender, EventArgs e)
{
var method = Request.HttpMethod;
        var action = Request.QueryString["action"];
        var startTime = DateTime.Now;
 
        if (method == "POST")
{
if (action == "slow")
System.Threading.Thread.Sleep(10 * 1000);
            Response.Write((DateTime.Now - startTime).TotalMilliseconds.ToString());
            Response.End();
        }
}
</script>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>wxapi</title>
<script src="http://static.fishlee.net/libs/jquery/1.12.4/jquery-1.12.4.min.js"></script>
</head>
<body>
<button id="normalRequest" type="button">常规请求</button>
<div></div>
<button id="slowRequest" type="button">慢速请求</button>
<div></div>
<script>
$(function () {
$("#normalRequest").click(function () {
$.post("?action=normal").always(function (data, option, xhr) {
$("#normalRequest + div").html(xhr.responseText || "empty.");
                });
            });
            $("#slowRequest").click(function () {
$.post("?action=slow").always(function (data, option, xhr) {
$("#slowRequest + div").html(xhr.responseText || "empty.");
                });
            });
        });
    </script>
</body>
</html>

并在 web.config 中开启跟踪。

<?xml version="1.0"?>
<configuration>
 <system.web>
     <httpRuntime targetFramework="4.5" />
     <trace enabled="true" requestLimit="100" pageOutput="false" />
 </system.web>
</configuration>

开启跟踪后,可以通过 /Trace.axd 查看跟踪结果。

首先,我在自己的手机微信里,分别点击了两个按钮。跟踪显示,只有两个记录,并且客户端IP都是和我本机吻合的。

然后我拿来他们测试有问题的手机,并在里面的微信中测试,分别点击。发现点击两次按钮,出现了三条记录。换句话说,问题复现了。

double-request-from-wechat-001

然后我们分别来看三个请求。

第一个请求对应的是短请求,也就是发送后立马成功的那种,相当无脑。

double-request-from-wechat-002

从此图我们很明显可以看出,远程地址是 123.151.42.57,这并不是本地宽带的出口IP。从上面的判断可知,这是中转服务器,并且是确凿的,为什么呢,因为最下面有个 HTTP_X__FORWARDED_FOR 这个标头。为什么这么说呢,因为这个标头简直是反代服务器或代理服务器的一种标志性特征,它是为了告诉上游服务器,其实它是转发别人的请求来着的。

然后我们来看第二个请求。

double-request-from-wechat-003

第二个请求是慢请求,除了操作参数不一样外,并没有不同,所有信息都和上面的请求完全一致。

最后我们来看第三个请求。

double-request-from-wechat-004

和之前的相比,典型的不同就是,客户端IP已经成为211.102.210.254,对应的就是本地的出口IP,换句话说,这是没有通过中转服务器而直接访问源服务器了。
因此,你也会看到下面的HTTP_X_FORWARDED_FOR标头已经不见了:因为不是中转了。

至此,所有推论得到了论证。

00746.gif

总结

复述一下第6节中的结论。

  1. 微信里直接发出ajax请求的话,其实是通过一些特定的服务器中转的
  2. 这个中转不是全部的,和手机以及系统有关系(因为我的安卓手机微信就没有这现象,而有此情况的手机也不是某个特定的品牌,iPhone也没此问题)
  3. 这个中转的超时时间很短,一旦超时,会迅速回滚为非中转模式请求

至于这个中转到底什么情况下有,什么情况下没有,这个没有找到规律,也未知是微信中内嵌的浏览器内核行为还是系统行为。

如果说非要做一点实质性的总结,那就是,如果假定你的请求是关键请求且难以重试还跑在微信中的,最好保证你的接口在两秒钟内返回,否则可能会有很诡异的难以复现的问题,如果时间过长的最好做成异步的。

结案陈词,就是这种中转机制设计真的很糙,也不知道是哪里引入的。

00628.gif

话说回来,既然有此机制,我觉得应该就有后门:通过啥参数能阻止此中转,然而并没有找到。如果有同学知道的话,烦请告知~

直到后来,用微信调试工具……

事实胜于雄辩,我觉得单看问题就能猜到原因,我也是牛逼啊……23333

double-request-from-wechat-005double-request-from-wechat-006

嗯,欢迎扫描下方二维码关注鱼的公众号(微信内长按识别哦),也感谢阅读本文。 4.gif

 

 

<think>嗯,用户问的是如何防护基于用户身份处理的横向越权。首先,我得先理解清楚什么是横向越权。横向越权指的是攻击者通过修改请求参数,比如用户ID、订单号等,来访问或操作其他用户的资源,而这些资源他们本来是没有权限访问的。这通常生在应用程序没有正确验证用户是否有权访问特定资源的情况下。 那防护措施应该从哪里入手呢?首先想到的是权限验证。每个请求在处理时,都必须检查当前用户是否有权限访问所请求的资源。比如,用户A试图访问用户B的数据,系统需要验证用户A的ID是否与用户B的ID匹配,或者用户A是否有其他权限(如管理员)可以访问。这一步需要在后端进行,不能依赖前端验证,因为前端容易被绕过。 然后是参数过滤。用户输入的参数,尤其是像用户ID、订单号这样的敏感信息,必须进行严格的校验。比如,检查参数是否是有效的格式,是否存在非法字符,或者是否属于当前用户的权限范围。比如,使用正则表达式验证用户ID是否为数字,或者检查该订单是否属于当前用户。 接下来是使用访问控制列表(ACL)或者基于角色的访问控制(RBAC)。ACL可以为每个资源设置具体的访问权限,而RBAC则根据用户的角色来分配权限。这样,系统可以根据用户的角色或ACL中的条目来判断是否有权执行操作。比如,普通用户只能访问自己的数据,而管理员可以有更广泛的权限,但即便如此,管理员的操作也应该记录和监控。 还有会话管理。确保每个会话都能正确识别用户身份,防止会话劫持或伪造。使用安全的会话管理机制,比如HTTPS来传输会话cookie,设置HttpOnly和Secure标志,防止XSS攻击窃取cookie。同时,会话过期时间不宜过长,减少被利用的机会。 另外,API设计也要注意。RESTful API中,资源URL应该包含用户的身份信息,比如/users/{userId}/orders,这样在处理请求时可以直接从URL中提取userId并与当前用户进行比较。避免使用顺序递增的ID,因为这样容易被猜测,可以使用UUID或者其他不可预测的标识符,降低枚举攻击的风险。 日志记录和监控也是重要的一环。记录所有敏感操作,包括谁在什么时候访问或修改了什么资源。一旦现异常访问,比如同一个用户短时间内访问大量不同用户的资源,可以及时报警并介入调查。日志需要妥善保存,防止被篡改,并且定期审计。 还有输入验证和输出编码。虽然主要是防御注入攻击,但严格的输入验证可以防止攻击者通过篡改参数进行越权。例如,确保用户ID是数字,并且符合预期的范围。输出编码则可以防止XSS攻击,避免攻击者通过注入脚本获取其他用户的会话信息。 另外,使用安全的开框架。很多现代框架内置了安全机制,比如Spring Security、Django的权限系统等,可以自动处理很多权限验证的问题,减少手动编写安全代码带来的漏洞。开者应该熟悉这些框架的安全特性,并正确配置和使用。 测试环节也不能忽视。进行渗透测试和代码审查,特别是针对权限验证的部分。自动化测试工具可以帮助现一些常见的漏洞,但人工审查同样重要,尤其是业务逻辑复杂的部分。单元测试和集成测试中应包含权限验证的测试用例,确保每个涉及用户资源的接口都经过严格的权限检查。 最后,用户教育和最小权限原则。确保用户了解不能分享自己的账户信息,同时系统设计时应遵循最小权限原则,用户只能拥有完成其任务所需的最小权限,减少横向越权可能带来的影响。 不过,可能在思考过程中会遗漏一些点,比如OAuth中的作用域(scopes)验证,或者JWT令牌中的声明是否正确处理。另外,分布式系统中如何保持权限的一致性,比如微服务之间如何传递用户上下文并验证权限,这也是需要考虑的。还有,前端在显示数据时是否可能泄露其他用户的信息,比如在列表中没有正确过滤数据,导致用户看到不属于自己的条目,虽然这属于纵向越权,但也需要注意。 总之,防护横向越权的核心在于后端严格的权限验证,不信任任何前端传入的参数,结合安全的编码实践和全面的测试,才能有效减少此类漏洞的风险。</think>防护基于用户身份处理的横向越权(Horizontal Privilege Escalation)的核心在于确保用户只能访问或操作自身权限范围内的资源。以下是关键防护措施及具体实施建议: --- ### **1. 严格的权限验证** - **后端强制校验**:所有涉及用户资源的请求(如访问用户ID、订单号等),必须**在后端**验证当前用户身份与请求资源归属是否匹配。例如: ```python # 示例:Django框架中检查用户权限 def get_order(request, order_id): order = Order.objects.get(id=order_id) if order.user != request.user: raise PermissionDenied("无权访问此订单") return order ``` - **避免依赖前端参数**:禁止直接使用前端传入的用户标识(如Cookie、URL参数)作为权限依据,需从认证凭据(如JWT、Session)中提取当前用户身份。 --- ### **2. 最小化敏感参数暴露** - **使用不可预测的标识符**:避免使用连续数字ID(如`user_id=1001`),改用UUID或哈希值,降低攻击者枚举其他用户资源的可能性。 - **参数过滤与校验**:对用户输入的资源标识进行格式验证(如正则匹配),并确保其属于当前用户权限范围。 --- ### **3. 基于角色与资源的访问控制** - **RBAC(基于角色的访问控制)**:定义清晰的角色(如用户、管理员),限制不同角色可访问的资源范围。 - **ABAC(基于属性的访问控制)**:结合用户属性(部门、职务)或资源属性(状态、敏感等级)动态控制权限。 - **ACL(访问控制列表)**:为每个资源单独配置允许访问的用户或角色。 --- ### **4. 安全的API设计** - **资源归属显式化**:在API路径中直接体现用户归属关系,例如: ``` GET /api/users/{current_user_id}/orders ``` - **限制批量查询接口**:避免返回全量数据,强制添加用户身份过滤条件,如SQL查询中自动附加`WHERE user_id = ?`。 --- ### **5. 会话与身份管理** - **强化身份认证**:使用OAuth 2.0、JWT等标准协议,确保令牌包含用户身份且不可篡改。 - **会话绑定**:将会话ID与用户IP、设备指纹等绑定,降低会话劫持风险。 --- ### **6. 日志与监控** - **记录敏感操作**:详细日志需包含操作者、时间、资源ID及操作类型,便于事后审计。 - **异常行为检测**:监控同一用户短时间内访问大量不同用户资源的行为,触告警。 --- ### **7. 安全开实践** - **使用安全框架**:如Spring Security、ASP.NET Core Identity,减少手动实现权限逻辑的漏洞。 - **代码审查与测试**: - **单元测试**:覆盖所有涉及权限校验的接口。 - **渗透测试**:模拟攻击者尝试横向越权,验证防护有效性。 --- ### **8. 其他关键措施** - **前端数据过滤**:即便后端安全,前端也应避免渲染其他用户敏感信息(如列表页仅显示当前用户数据)。 - **错误信息模糊化**:返回“404未找到”而非“403无权限”,避免泄露资源存在性。 --- ### **典型场景示例** - **场景**:用户A尝试通过修改URL参数`user_id=123`访问用户B的数据。 - **防护**: 1. 后端从认证令牌提取当前用户ID(如用户A)。 2. 查询目标资源(用户B的数据)时,验证资源是否归属于用户A。 3. 若不符,返回“404 Not Found”或“403 Forbidden”。 --- 通过**多层级验证**、**最小权限原则**和**纵深防御**策略,可有效遏制横向越权漏洞。核心原则是:**永远不信任客户端传递的任何身份相关参数**,所有权限判断必须由服务端基于可信身份信息完成。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值