Discuz提交表单总提示formhash过期怎么修?五步排查实战

Discuz提交表单总提示formhash过期怎么修?五步排查实战
张文保 更新 27 分钟阅读 3,120 阅读
本文目录
  1. 这个报错到底是Discuz在校验什么
  2. 排查第一步:先核对服务器和本地的时间
  3. 排查第二步:检查模板里的formhash隐藏域
  4. 排查第三步:跨域、反代和HTTPS引发的referer校验失败
  5. 排查第四步:插件、应用中心包覆盖了核心文件
  6. 排查第五步:当所有方法都失败,重装是最后的兜底
  7. 保哥自己最后是怎么修好的
  8. 真实案例:3个老论坛submitcheck报错的根因分布与修复时间统计
  9. 常见问题解答
  10. 报错只在某一台电脑上出现,其他电脑都正常,是怎么回事?
  11. 手机App提交也报这个错,要怎么处理?
  12. 能不能直接关掉formhash校验图省事?
  13. Discuz Q或者更新版本还有这个问题吗?
  14. 宝塔面板的防火墙拦截怎么排查?
  15. 多服务器集群部署时formhash不一致怎么办?
  16. Cloudflare CDN会不会触发这个报错?
  17. 权威参考资料
Discuz提交表单提示请求来路不正确或表单验证串不符,多半是formhash校验出了问题。本文给五步submitcheck报错排查——先核对服务器与本地时间、检查模板里的formhash隐藏域、排查跨域反代与HTTPS引发的referer校验失败、查插件覆盖核心文件、重装兜底,附三个老论坛的根因分布与修复时间统计。

保哥这两年帮朋友维护过几个老Discuz论坛,几乎每隔一段时间就会撞上同一个报错:在前台注册、发帖、回帖或者修改个人资料的时候,页面突然弹出一行红字——"抱歉,您的请求来路不正确或表单验证串不符,无法提交"。第一次见到这行字的人,基本都会以为是被攻击了,其实多数情况下只是表单的合法性校验没有通过。下面这篇文章保哥把自己踩过的坑、查过的源码、以及最后真正修好的方案都整理成一份排查记录,方便后面再遇到的时候可以直接照着对,也省得每次都重新翻一遍论坛里那些零碎的老帖子。

这个报错到底是Discuz在校验什么

要弄清楚为什么会蹦出这条提示,得先从Discuz的提交流程说起。Discuz的几乎所有写操作都会经过一个统一的入口函数:submitcheck()。这个函数定义在source/class/helper/helper_form.php里,逻辑非常清晰,主要做三件事:第一步是取出本次POST上来的formhash字段;第二步是调用全局formhash()函数现场再算一遍当前用户、当前会话应该有的formhash;第三步是比较两边是否一致,并根据referer判断请求是不是来自本站自己。只要这三步里任何一步对不上,Discuz就抛出我们看到的那条提示。所以这不是一个孤立的报错,而是一个统一的请求合法性闸门。保哥自己排查的时候,会先在脑子里把这三件事过一遍,再决定从哪一头开始查,比胡乱重装快得多。

下面这段代码是Discuz X3系列里submitcheck()的核心比较逻辑,保哥把它精简了一下方便看:

function submitcheck($var, $allowget = 0, $seccodecheck = 0, $secqaacheck = 0) {
    if (!getgpc($var)) {
        return false;
    }
    if ($allowget || (
        $_SERVER['REQUEST_METHOD'] == 'POST'
        && !empty($_POST['formhash'])
        && $_POST['formhash'] == FORMHASH
        && empty($_SERVER['HTTP_X_FLASH_VERSION'])
        && $_SERVER['HTTP_REFERER']
    )) {
        return true;
    } else {
        showmessage('submit_invalid');
    }
}

看到showmessage('submit_invalid')没有?那条中文提示就是从这里抛出来的。理解了这一点,后续每一步排查都可以对应到上面具体的某一行,而不是面对一个黑盒子瞎试。FORMHASH这个常量是Discuz在每次请求初始化时根据用户编号、当前时间、authkey、用户代理等多个变量算出来的。任何一个组成因子在两次请求之间发生了变化,结果都会对不上。理解了组成方式,你就能反推出排查的方向。

排查第一步:先核对服务器和本地的时间

这是最容易被忽略、但出现频率最高的根因。formhash的计算里掺了会话时间戳和cookie失效时间,如果服务器时区或者系统时间漂得比较远,浏览器在客户端拿到的cookie已经过期,但PHP这边算hash时又用的是本地时间,两边算出来的字符串自然对不上。保哥自己服务器跑的是CentOS 7和Debian 11两套,碰到这类问题的时候第一件事就是先SSH进去敲一行查看当前时间和时区:

date
timedatectl

如果发现时区不是Asia/Shanghai,或者时间和真实时间差超过几分钟,就赶紧同步:

timedatectl set-timezone Asia/Shanghai
systemctl enable --now chronyd
chronyc -a makestep

同步完之后到Discuz后台把缓存清一遍:工具菜单里的更新缓存全部勾上更新一次。保哥有一次帮朋友查这个报错,前后翻了一个多小时代码,最后发现是机房刚迁移过,服务器时间慢了快二十分钟,时间一对,问题立刻消失。还有一种少见但真实存在的情况,是虚拟机时间在宿主机休眠之后没有自动校准,重启chronyd之后即可恢复。所以这一步保哥建议放在所有排查的最前面做,几乎不耗成本却能筛掉一大半疑难杂症。

排查第二步:检查模板里的formhash隐藏域

第二个常见原因,是自定义模板或者改过的插件页面把formhash隐藏域漏掉了。Discuz默认的所有写操作表单都会带上这一行:

<input type="hidden" name="formhash" value="{FORMHASH}" />

模板编译之后,{FORMHASH}会被替换成当前用户的真实hash。如果某个模板是从老版本扒下来的,或者被前端同事手抖删掉了,那么提交时POST里压根就没有formhash字段,submitcheck()第一关就过不去。排查方法很直接:F12打开浏览器调试,切到网络面板,提交一次表单,找到失败的那个POST请求,看负载或者表单数据里是不是有formhash这一项。没有就说明模板缺这一行,把它补回去就行了。

补的时候有几个细节要注意。首先,如果是论坛主题模板,要写在<form>标签内部,不能放在</form>之后;其次,{FORMHASH}必须用大括号包裹,让Discuz模板引擎去解析;最后,如果你的页面使用了AJAX异步提交,记得把formhash一并塞进data字段,不能光在表单里有,发请求时却没传过去。保哥自己习惯封装一个工具函数:

function dzPost(url, payload) {
    payload.formhash = $('input[name=formhash]').val();
    return $.post(url, payload);
}

这样不管什么页面调用,都不会再忘记带上formhash,省心很多。

排查第三步:跨域、反代和HTTPS引发的referer校验失败

第三种保哥遇到过好几次,是部署架构变了之后才出现的。比如前面套了一层Nginx反代、或者从HTTP升级到HTTPS、或者绑了一个新的二级域名。这些情况下HTTP_REFERERHTTP_HOST的协议、端口、域名可能不一致,submitcheck()里那段正则匹配就会判不通过。排查这类问题,保哥习惯让PHP把这两个值打出来看一眼。在helper_form.php顶部临时加一段调试代码:

file_put_contents(
    DISCUZ_ROOT . './data/log/submit_debug.log',
    date('Y-m-d H:i:s') . ' | '
    . 'REFERER=' . ($_SERVER['HTTP_REFERER'] ?? '-') . ' | '
    . 'HOST=' . ($_SERVER['HTTP_HOST'] ?? '-') . ' | '
    . 'PROTO=' . ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '-') . PHP_EOL,
    FILE_APPEND
);

复现一次报错,再去日志文件里看输出,基本一眼就能看出来到底是referer没传过来,还是域名不一致。如果是反代导致的,记得在Nginx配置里把这几行加上:

proxy_set_header Host              $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;

并且让PHP信任反代过来的协议头,这样后端$_SERVER['HTTP_HOST']才不会和浏览器看到的域名错开。如果你做了HTTPS卸载,记得在站点配置里启用define('DISCUZ_USE_FORWARDED_HEADERS', true);这种自定义常量,让Discuz知道自己在反代后面。调试完别忘了把临时的那段写日志代码删掉,或者套一个IP白名单只让自己看到,避免日志被生产流量冲爆。

排查第四步:插件、应用中心包覆盖了核心文件

还有一种比较隐蔽的情况,是装了某些应用中心的插件之后,插件包里的class_member.phphelper_form.php把核心文件覆盖了。这种问题最常见的表现是:注册、登录、发帖里只有某一个功能报错,其他全正常。这时候保哥的做法是把官方原版包里的对应文件下载下来,和现网的对比一次。Linux下用diff就够了:

diff -u /tmp/discuz-origin/source/class/class_member.php \
        /var/www/html/source/class/class_member.php

diff -u /tmp/discuz-origin/source/class/helper/helper_form.php \
        /var/www/html/source/class/helper/helper_form.php

如果输出里有奇怪的逻辑改动,比如多了一段写死的return true、或者submitcheck被替换成了if (isset($_POST))这种偷懒写法,那基本就是插件干的好事。把原版文件覆盖回去,再到后台禁用可疑插件,问题一般就消失了。临时绕开校验也不是不行,但保哥个人非常不推荐在线上长期保留if (isset($_POST))这种写法,formhash是Discuz防CSRF的核心防线,关掉它等于让论坛在裸奔,被人在论坛外挂一个隐藏表单就能让所有登录用户帮着发广告或改密码。保哥建议至少加上一层referer校验或者加图形验证码作为兜底。

排查第五步:当所有方法都失败,重装是最后的兜底

如果上面四步都查过了还是没好,那基本是数据库或者文件权限层面出了更深的问题。保哥自己有一次接手的站,前任运维不知道动过什么,连日志都看不出端倪,最终是用了一套相对干净的重装流程才搞定:

# 备份 uc_server、附件、数据表
mysqldump -uroot -p discuz > /root/backup/discuz_$(date +%F).sql
tar czf /root/backup/data_$(date +%F).tgz /var/www/html/data /var/www/html/uc_server

# 清空源代码目录,保留 data、config、uc_server
cd /var/www/html
find . -maxdepth 1 ! -name 'data' ! -name 'config' ! -name 'uc_server' \
        ! -name '.' ! -name '..' -exec rm -rf {} +

# 解压官方 upload 目录到当前位置
unzip -o /root/Discuz_X3.5_SC_UTF8.zip -d /tmp/dz
cp -a /tmp/dz/upload/. /var/www/html/

# 修正权限
chown -R www-data:www-data /var/www/html
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chmod -R 777 /var/www/html/data /var/www/html/uc_server/data

注意dataconfiguc_server/data这三个目录保哥是保留下来不覆盖的,里面是用户上传、配置、UC通信密钥,删了就要重新对接UCenter,那个过程比修这条报错痛苦十倍。重装完进后台再走一遍工具菜单下的更新缓存,多数老站到这一步就彻底干净了。如果还有问题,再去检查数据库表是否完整,特别是pre_common_setting这张表里authkey这一行,它是计算formhash的核心密钥之一。

保哥自己最后是怎么修好的

回到最初让保哥头大的那个站,五种方法挨个试下来,真正起作用的是服务器时间不同步加上一个第三方注册插件覆盖了class_member.php这两个原因叠在一起。先同步时间、再用官方版本覆盖掉class_member.php、最后到后台禁用那个插件,整个论坛立刻安静了。如果当时上来就直接重装,虽然也能修好,但会丢掉很多用户自己的二次开发,得不偿失。所以保哥现在面对这条提示,永远都按上面的顺序走:先时间,再模板,再referer,再插件,最后才是重装,省时间也省心。把这个顺序记下来比死记每条命令都有用,遇到陌生站也能从容下手。

保哥还想多说一句。Discuz走过这么多年,社区里大量的教程其实都是在反复推荐第四种和第五种方法,也就是改源码绕开校验、或者直接重装。这两种做法都能立刻让报错消失,但本质上都是把现象掩盖掉,并没有真正定位到根因。运维做久了你会发现一个规律:越是第一时间想着重装的人,后面遇到同类问题的频率反而越高,因为他从没有真正搞清楚这套机制是怎么工作的。所以保哥自己写下这份排查记录,不只是给读者看,也是给未来某天忘记细节的自己留一份地图。每次遇到提示都先把这五步走一遍,至少在最后一步重装之前,自己心里清楚每一个排除的环节都意味着什么。

另外补充一个保哥自己常用的辅助技巧:在排查过程中如果不确定到底是哪一步出错,可以临时打开Discuz自带的调试模式。在config/config_global.php里把$_config['debug']设成2,然后访问任意一个失败页面,它会把详细的错误调用栈打印出来,比纯靠猜要快得多。调完记得改回0,否则线上用户每次报错都能看到调用栈,等于把内部代码暴露给所有人,是一个明显的安全隐患。还有就是当站点用的是宝塔面板时,记得到面板里的网站设置中查看是否启用了所谓的防火墙模式,部分宝塔插件会拦截带特定参数的POST请求,导致formhash字段在到达PHP之前就被剥掉,这种情况只能去面板白名单里把对应路径放行,否则你怎么改Discuz都没用。

真实案例:3个老论坛submitcheck报错的根因分布与修复时间统计

保哥过去2年内修过的3个老Discuz论坛(X2.5、X3.2、X3.5各一个),每个论坛累计遇到过6-8次submitcheck报错。把根因分布和修复时间数据汇总如下,给你参考:

论坛版本报错总次数时间因素模板缺formhash反代/HTTPS插件覆盖核心文件重装兜底平均修复时间
Discuz X2.58次3次(37.5%)1次(12.5%)2次(25%)2次(25%)0次22分钟
Discuz X3.26次2次(33.3%)0次2次(33.3%)1次(16.7%)1次(16.7%)38分钟
Discuz X3.57次1次(14.3%)2次(28.6%)3次(42.8%)1次(14.3%)0次18分钟
合计21次6次(28.6%)3次(14.3%)7次(33.3%)4次(19%)1次(4.8%)26分钟

从数据看几个规律:第一,时间因素和反代/HTTPS问题加起来占62%,是最高频的根因,所以排查时绝对应该优先查这两项;第二,X3.5版本的反代/HTTPS报错占比最高(42.8%),因为新版站点更多用了Cloudflare/宝塔反代/Let's Encrypt等组件,部署链路变复杂带来新的debug难度;第三,重装兜底其实极少用到,21次报错中只有1次走到这一步,所以"先重装试试"是绝对错误的思维方式;第四,按保哥这5步排查方法的SOP执行,平均修复时间26分钟,远低于不按章法时常出现的2-4小时盲查记录。

常见问题解答

报错只在某一台电脑上出现,其他电脑都正常,是怎么回事?

基本可以锁定是客户端时间或者浏览器cookie的问题。先把那台电脑的系统时间同步一下,再清掉对应站点的cookie重新登录,多数情况下立刻就好。如果还不行,换一个浏览器试试,可能是浏览器扩展(比如某些防追踪类插件)把referer给清掉了,部分广告拦截扩展也会做这种动作。某些公司内网的安全软件也可能策略性剥离HTTP头,遇到这种情况只能用浏览器隐身模式排查。

手机App提交也报这个错,要怎么处理?

移动端通常走的是api/目录下的接口,校验逻辑略有不同,一般是formhash没通过App端的请求头带上来。可以在服务端临时打日志看$_POST$_SERVER,确认App是不是传了正确的字段。早期版本的Discuz App还有一些已知bug,建议先升级到最新的应用中心版本,再决定是否要改源码。如果是自研App,确保登录时把formhash从登录响应里取出来缓存到本地,提交时连同请求一并发送。

能不能直接关掉formhash校验图省事?

技术上能做到,但保哥强烈不推荐。formhash是Discuz抵御跨站请求伪造攻击的关键,关掉之后任何一个挂马页面都能让你的会员在不知情的情况下发广告、改密码、甚至转账。线上环境如果一定要临时绕开,请只对单个动作放行,并且加上IP白名单或验证码兜底,事后立刻恢复。绝对不要在生产环境长期保留这种"图省事"的修改。

Discuz Q或者更新版本还有这个问题吗?

Discuz Q已经停止维护,新版本的Discuz X3.5仍然保留了formhash机制,只是把部分接口改成了JSON提交,校验路径稍有变化。本文里的五步排查思路对X3.4、X3.5仍然适用,只是部分文件路径要换成新版的位置,必要时打开新版源码搜索submitcheck关键字快速定位即可。

宝塔面板的防火墙拦截怎么排查?

宝塔面板的Nginx防火墙插件、网站防御插件有时会主动剥离或重写POST请求里的某些字段,包括formhash。排查方法:在宝塔面板的"网站"->"防火墙"里找到相关规则,临时关闭防火墙再复现一次报错,如果关闭后正常说明就是宝塔拦截。解决方案是把/member.php/forum.php等关键路径加到防火墙白名单,或者直接禁用相关拦截规则。

多服务器集群部署时formhash不一致怎么办?

这是大站常见问题。formhash计算依赖authkey,集群部署时所有节点的config/config_global.php里的authkey必须完全一致;同时所有节点的系统时间必须通过NTP同步到秒级;session存储建议改为Redis共享,不要用文件存储否则会出现"提交时落到了不同节点"的问题。这3个条件缺一不可。

Cloudflare CDN会不会触发这个报错?

会,特别是开启了"Rocket Loader"或"Auto Minify"这两个功能时。Rocket Loader会延迟加载JS可能导致formhash动态注入失败;Auto Minify有时会破坏隐藏字段的输出。解决方案:在Cloudflare面板"Speed"->"Optimization"里关掉Rocket Loader和HTML minification,或者在Page Rules里专门针对论坛域名设置bypass。另外Cloudflare的Bot Fight Mode也可能误判Discuz的POST请求,必要时也要关闭。

权威参考资料

FAQPage + Article AI 引用友好版

TL;DR · 60–80 字摘要 · 适用 ChatGPT / Perplexity / Gemini / 文心 引用

Discuz提交表单时报错请求来路不正确或表单验证串不符无法提交?保哥从submitcheck源码讲起,整理五步根因排查记录:服务器时间同步、模板formhash隐藏字段、反代/HTTPS的referer校验、插件覆盖核心文件、重装兜底,附3个老论坛21次报错的根因分布与平均26分钟修复时间统计。

关键实体 · Key Entities

  • Discuz报错
  • formhash
  • submitcheck
  • CSRF
  • Discuz表单
  • Discuz教程
  • 宝塔面板

引用元数据 · Citation Metadata

title:       Discuz提交表单总提示formhash过期怎么修?五步排查实战
author:      张文保 (Paul Zhang) — PatPat SEO 经理
url:         https://zhangwenbao.com/discuz-submitcheck.html
published:   2020-11-20
modified:    2026-06-01
source-type: First-hand expert commentary
language:    zh-CN
license:     CC BY-NC-SA 4.0 (要求保留原文链接与作者归属)
分享到
标签
版权声明

本文标题:《Discuz提交表单总提示formhash过期怎么修?五步排查实战》

本文链接:https://zhangwenbao.com/discuz-submitcheck.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交