一个“403”问题的产生及解决

最近在测试一个项目的时候,遇到了一个比较诡异的“403”问题问题。在经过不断的查找资料和咨询大师级的人物之后,问题终于有了解决方案。现在就把我在整个过程中遇到的坑记录下来,可以让大家后续遇到类似问题有所参考。

 

问题描述:

1.       测试的项目是一个程序设计考试的web项目,因此会涉及到用户的登录,查看题目,提交答案,查看题目列表,查看排名等一系列问题;

2.       项目开始时,只有一台服务器,这里称为服务器A,包括了web + 判题服务两部分。为了提高项目的性能,新增加了两台服务器,这里称为服务器B和服务器C,同样上面也都部署了web + 判题服务两部分;

3.       在扩容前,也就是只有服务器A的时候,测试脚本中的请求发送的URL就是服务器AIP地址+开放的端口号,这里就用“服务器A_IP :port”来表示;

4.       在扩容之后,因为有了服务器A,B,C,势必需要进行负载均衡,于是引入了nginx来做负载均衡。所以在nginx中配置了域名server_name。那么理所当然的,我的测试脚本中的请求发送的URL就是nginx中设置的域名了,这里就用“server_name”来表示;

5.       那么问题终于来了,扩容前进行测试一切请求包括,用户的登录,查看题目,提交答案,查看题目列表,查看排名等都很正常。但是扩容之后,在提交答案的这个请求返回状态一直是“403 forbidden”。并且扩容前后测试脚本并没有改动,唯一的改动地方就是请求的URL从“服务器A_IP :port”变成了“server_name”;

 

解决过程:

1.       刚看到这个问题,第一个想法是这样:


由于nginx做了负载均衡,那么一个脚本中的3个请求:request1request2request3分别给均衡到了服务器A,服务器B,服务器C上面。所以对于用户perftest1来说,他的登录请求被分配到了服务器A上,查看题目的请求被分配到了服务器B上,提交答案的请求被分配到了服务器C上。被分配到服务器C上的请求request3,仍然在苦苦等着属于他自己的patsid,但是他自己的patsid却在服务器A上,他永远也拿不到,所以产生了 403请求,被服务器以不正确的patsid为由拒绝了perftest1的提交答案的请求。

   请教了一些同事后,他们给的建议是采用nginxsticky模块。在多台后台服务器的环境下,我们为了确保一个客户只和一台服务器通信,我们势必使用长连接。使用什么方式来实现这种连接呢,常见的有使用nginx自带的ip_hash来做,我想这绝对不是一个好的办法,如果前端是CDN,或者说一个局域网的客户同时访问服务器,导致出现服务器分配不均衡,以及不能保证每次访问都粘滞在同一台服务器。如果基于cookie会是一种什么情形,想想看, 每台电脑都会有不同的cookie,在保持长连接的同时还保证了服务器的压力均衡,nginx sticky值得推荐。如果浏览器不支持cookie,那么sticky不生效,毕竟整个模块是给予cookie实现的。nginx sticky 模块工作流程图如下:


但是,在nginx的配置文件中新增了sticky模块之后,问题并没有解决。方案一以失败告终。

2.  采用tcpdump抓包查看

在服务器端添加了日志,然后用“服务器A_IP :port”作为请求URL的时候,日志中的Parameters如下所示:

Parameters: {"utf8"=>"?", "authenticity_token"=>"pcL2+QNvRS0/eOgoO+L/PuLrWOo7YEHj1kf+pPrLsT4=", "compiler_id"=>"2", "advanced_editor"=>"1", "code"=>"[FILTERED]", "commit"=>"Ã\u008Cá½»´úÃ\u0082ë", "id"=>"118", "link_name"=>"A"}

{"warden.user.user.key"=>["User", [62076], "2a10$hQRBDYakeF5M3Mfk/8i19O"], "flash"=>#<ActionDispatch::Flash::FlashHash:0x0055c440663ed8 @used=#<Set: {}>, @closed=false, @flashes={}, @now=nil>, "_csrf_token"=>"pcL2+QNvRS0/eOgoO+L/PuLrWOo7YEHj1kf+pPrLsT4="}

[["patsid","3678395b1431877951ee9245e79cbe98"]]

当用“server_name” 作为请求URL的时候,日志中的Parameters如下所示:

 Parameters: {"compiler_id"=>"2", "advanced_editor"=>"1", "code"=>"[FILTERED]", "commit"=>"Ã\u008Cá½»´úÃ\u0082ë", "id"=>"118", "link_name"=>"A"}

{}

[["f78494dc412a0455a5b67f68707b2b97",null]]

很明显,用“server_name” 作为请求URL的请求参数中cookiesession都是空的,这也难怪会被403,给拒绝掉。

但是我的测试脚本中明明是有Set-Cookie的。为了一探究竟,用tcpdump进行抓包:


可以发现cookie中的参数是我在脚本中set进去的,登录获得的patsid。但是却被服务器拒绝掉了。

3.  终于查到根源

咨询了nginx大牛——刘成同学,终于找到了nginx403问题的根源了,问题就在出现在请求参数的header中的这个参数'authenticity_token'

因为nginx中的自身很多变量都是以下划线标识的,所以遇到这个'authenticity_token'的时候,nginx为了防止混淆,他会把用户发过来的过滤掉,所以就识别不到这个token值。解决方法是在nginx的配置的http模块中添加  underscores_in_headers on;(这个默认值是关闭的) 然后重启nginx之后,就可以用脚本返回正常的请求结果了。

HTTP头部是否允许下划线

语法:underscores_in_headers on | off;

默认:underscores_in_headers off;

配置块:httpserver

默认为off,表示HTTP头部的名称中不允许带“_”(下划线)。

可以查看ngx_http_core_module 模块的说明。

 

至此,一个由nginx引发的403问题终于解决掉了,希望能给大家以帮助。

本文来自网易实践者社区,经作者齐红方授权发布。