当 ajax 遇到 cloudflare 5秒盾

事情要从最近社区有个别佬友遇到点赞报错、小蓝点不消失说起。这个问题简单来说就是:有 ajax 请求撞到 cloudflare 的5秒盾,导致请求被拦截。

社区安全规则中是有静默5秒盾的(具体参考我之前写的帖子:【丧心病狂】给你网站所有接口加上验证码 ),一般来说都是可以静默自动过,但有少量浏览器/插件/网络原因,会导致自动过不了。这些接口不同于页面(更有非GET请求),我们无法通过页面打开的方式来解决质询。遇到这个问题,有佬友会推荐到 cdk 页面中过盾,其实利用了 cf_clearance cookie 域是 .linux.do 实现共享手动过盾。

这就引入了另一个问题,接口类型如何更友好地引导过盾,而不是直接被拦截?

要解决这个问题也很简单。

cloudflare 在需要质询的访问中,会返回一个http header:cf-mitigated,值一般为:challenge,同时状态码会是 4xx(不一定是403,可以 waf 中自定义)。所以,我们对 ajax/fetch 的错误回调进行 hook,判断到这些特征,就知道访问被盾了。

再配合一个始终有盾的地址(我们称为 challenge 地址,GET访问),我们可以取当前页面地址(非 ajax 请求接口地址),跳转到这个 challenge 址手动解决5秒盾的质询。解决5秒盾之后,将能访问到 challenge 地址,在其逻辑中直接跳回原页面地址即可。


基于这个技术原理,我为 discourse 写了一个插件来做这个事情。

前端js代码:

import {withPluginApi} from "discourse/lib/plugin-api";
import getURL from "discourse/lib/get-url";
import $ from "jquery";

function initializeCloudflareHandler(api, container, siteSettings) {
    function isCloudflareChallengeResponse(xhr) {
        return xhr && "challenge" === xhr.getResponseHeader("cf-mitigated");
    }

    function handleCloudflareChallenge() {
        window.location = getURL(`/challenge?redirect=${encodeURIComponent(window.location.href)}`);
    }

    $(document).ajaxError((event, xhr, ajaxSettings, thrownError) => {
        if (isCloudflareChallengeResponse(xhr)) {
            event.stopImmediatePropagation();

            handleCloudflareChallenge();
        }
    });
}

export default {
    before: 'inject-objects',
    name: "extend-for-cloudflare-handler",

    initialize(container) {
        const siteSettings = container.lookup("site-settings:main");
        if (!siteSettings.cloudflare_handler_enabled) {
            return;
        }

        withPluginApi("0.8.28", api => initializeCloudflareHandler(api, container, siteSettings));
    }
};

插件 plugin.rb 代码:

enabled_site_setting :cloudflare_handler_enabled

after_initialize do
  Discourse::Application.routes.append do
    get "/challenge" => "cloudflare_challenge#index"
  end

  class ::CloudflareChallengeController < ::ApplicationController
    skip_before_action :check_xhr
    skip_before_action :redirect_to_login_if_required
    skip_before_action :verify_authenticity_token

    def index
      redirect = params[:redirect]
      home_url = Discourse.base_url

      begin
        if URI.parse(redirect).host == URI.parse(home_url).host && redirect.start_with?(home_url)
          redirect_to redirect
          return
        end
      rescue
        # ignore
      end

      redirect_to home_url
    end
  end
end

大概就是注册并拦截 ajax 错误,判断访问被盾,就跳转到我们定义的 challenge 地址。

challenge 中判断有合法的回跳地址,就跳回,没有就跳首页。当然,challenge 地址并非必须后端实现,前端 js 也能做这个活,但因为这里需要一个路由,就简单在路由里做掉了。


如此,下次点赞时遇到没能自动过盾,应该会直接带你去过盾地址。
当然,你的浏览器/插件/网络还是有可能让你手动也过不了盾,那就是另一个问题了 :tieba_087:

583 个赞

第一先拿了

20 个赞

前排围观

10 个赞

:sob: :sob: :sob:

没抢到沙发,不过“零食饮料矿泉水,瓜子花生八宝粥”还是有的

这个好!这个好!不用怕点赞的时候突然 403 了

15 个赞

好久没来技术贴了

45 个赞

前排围观

7 个赞

第四预定了

6 个赞

前排来了,跟着始皇学技术

4 个赞

前排前排,前排出售西瓜,瓜子,广告位招租 :tieba_025:

4 个赞

我还是过不了你心里的盾

81 个赞

这下终于不用卡点赞了 :partying_face:

2 个赞

第10个赞拿了

2 个赞

后排围观 :tieba_087:

1 个赞

好快啊 佬友们
始皇多发点技术的。爱看~

2 个赞

这就是高手

1 个赞

前排围观

1 个赞

并非个别,而是很多

4 个赞

我还在前排,快占位置

1 个赞

围观围观

1 个赞

始皇研究透了 :clap:

1 个赞