事情要从最近社区有个别佬友遇到点赞报错、小蓝点不消失说起。这个问题简单来说就是:有 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 也能做这个活,但因为这里需要一个路由,就简单在路由里做掉了。
如此,下次点赞时遇到没能自动过盾,应该会直接带你去过盾地址。
当然,你的浏览器/插件/网络还是有可能让你手动也过不了盾,那就是另一个问题了 ![]()
