从WordPress插件到跨站SSO:我的问卷平台打通之路

Enjoy
Enjoy
Enjoy
管理员
22
文章
0
粉丝
编程笔记评论26阅读模式
摘要说干就干。从2024年底到2025年初,我用原生PHP从零搭了一套问卷系统。35个PHP文件,9200多行代码,支持单选题、多选题、填空题、评分题、矩阵题、下拉题、文件上传7种题型...

起因:一个WordPress插件的折腾史

说起来,我做问卷系统的念头其实挺简单的。2024年下半年,我琢磨着给影图资源站(yingtux.cn)加个用户反馈功能,能收集访客意见、做个简单的投票调查什么的。一开始图省事,直接装了个WordPress问卷调查插件——WP Survey System

插件装上第一天就给我来了个下马威。启用之后整个站点直接白屏,报Fatal Error。我赶紧去后台看日志,发现是插件里有个Database类跟我的WordPress版本不兼容。翻了半天GitHub Issues,才找到解决方案,手动改了两行代码才让网站恢复。这才只是开始。

接下来填问卷,选项居然提交不上去。用户在表单里选了A、B、C,提交后数据库里只存了空值。调试了一晚上,发现是SQL语句里少了引号转义,中文选项直接让MySQL报错了。统计页面更离谱,饼图渲染出来的数据永远对不上——数组索引从0开始,但后端逻辑默认从1开始读取。

这还没完。最让我崩溃的是中文编码问题。我用宝塔面板上传了14个PHP文件,结果上传过程中编码全部损坏,所有中文注释和字符串都变成了乱码。我对着屏幕愣了五分钟,最后一咬牙:全改成英文变量名、英文注释。这一改又是大半天,改完之后插件总算能跑通了。

但跑通之后我开始认真思考一个问题:WordPress真的太重了。为了一个问卷功能,要维护一整套WordPress生态,插件升级还要担心兼容性问题。用户的反馈问卷本来就应该轻量、简单,为什么要被WordPress绑架?

于是我做了个决定:脱离WordPress,做一个独立的问卷SaaS平台。

说干就干。从2024年底到2025年初,我用原生PHP从零搭了一套问卷系统。35个PHP文件,9200多行代码,支持单选题、多选题、填空题、评分题、矩阵题、下拉题、文件上传7种题型。还做了卡密系统和三级权益体系——免费用户能做基础问卷,付费用户能解锁更多模板和统计分析功能。移动端适配也做了,表单在手机上显示得还算体面。

最终部署到 survey.sjinyu.com,终于有了自己的问卷平台。打开网站的那一刻,我还是挺有成就感的。

新问题:两个站,两套账号

survey.sjinyu.com 跑起来了,用户可以在上面注册、创建问卷、发布链接、查看统计。一切看起来都很美好。

直到有一天我自己测试的时候发现了一个致命的问题:sjinyu.com 和 survey.sjinyu.com 是两个完全独立的系统。

我在sjinyu.com注册了一个账号叫Joyin2026,在survey注册了一个账号叫jinyu2026。有一天我想在survey里创建一个问卷,结果发现我得重新注册一个账号、重新设置密码。每次要登录两个站,退出也是各退各的。

这种体验让我很不舒服。明明是同一个人的两个站,为什么要管理两套用户名密码?用户来填问卷的时候,如果是通过sjinyu.com的活动入口进来的,凭什么还要再注册一遍?两个站的数据虽然物理上隔离了,但用户认知里,它们都属于"卓影数字传媒"这个品牌。

这个体验上的割裂感让我开始思考一个问题:能不能让用户在sjinyu.com登录之后,直接免登录访问survey?

方案选型:SSO是不是必须用OAuth2?

说起来,SSO(单点登录)这个概念我早就知道,但真正要自己实现的时候才发现水很深。

第一种方案是OAuth2。GitHub登录、Google登录都是这个套路。但我仔细研究了一下,OAuth2需要一整套授权码回调流程,还需要维护client_id和client_secret,还需要处理token刷新。survey只是一个问卷小工具,引入这么重的体系有点杀鸡用牛刀。

第二种方案是SAML。这玩意儿更企业级了,配置复杂得我看了就头疼。适合那种有几万员工的跨国公司,不适合我这种个人开发者。

第三种方案是自己搭JWT服务。用户登录之后颁发一个JWT token,其他站点验证token即可。但JWT需要额外的签发服务,还需要处理token过期和吊销,对我目前的架构来说改动太大。

转念一想,我的需求其实没那么复杂:

  • sjinyu.com是我的主站,所有用户数据都在那里
  • survey.sjinyu.com是从站,它本身也有自己的用户体系(为了方便独立访问的用户)
  • 两个站之间需要能"传递"登录状态

最终我设计了一套轻量级的Token中转方案,核心思路是:

  1. 主站sjinyu.com作为认证中心(IdP):负责验证用户身份、颁发临时凭证
  2. 从站survey作为服务提供者(SP):接收凭证、验证后建立本地session

具体流程是这样的:

用户 → 访问survey.sjinyu.com → 点击"卓影账号登录"→ 跳转到
sjinyu.com/authorize.php?app_key=xxx&redirect_uri=xxx&state=xxx
→ 用户在主站已完成登录 → 主站生成一次性token → 回调到survey的verify.php
→ survey验证token签名 → 查询用户信息 → 建立本地session → 完成登录

关键的安全设计:

  • 一次性token:每个token只能用一次,用完立即失效
  • 5分钟有效期:防止token泄露后被滥用
  • HMAC-SHA256签名:每个请求都带签名,验证请求来源
  • state参数:防止CSRF攻击
  • 邮箱关联:如果从站已有该邮箱的账号,直接关联;没有则自动创建

设计完之后我长舒一口气:这个方案比OAuth2简单得多,不需要第三方库,不需要额外的认证服务,但安全性上该有的都有。

踩坑实录:每一个Error都让我成长

坑1:Database类不存在

方案设计完了,我开始写代码。authorize.php和verify.php需要连接主站的数据库读取用户信息。我下意识地用了mysqli风格的代码:

$db = new Database();$user = $db->query("SELECT * FROM users WHERE email = ?", $email);

结果一跑起来,Fatal Error:Class 'Database' not found。

我愣了一下才反应过来:sjinyu.com的数据库操作层用的是PDO,而我习惯性地用了survey里自己封装的Database类。但authorize.php和verify.php是主站的代码,不在survey的框架里,当然找不到Database类。

解决方案很直接:全部重写成PDO版本。这个坑让我意识到一个很重要的事情——代码不能想当然复用,每个项目的类库都是独立的

坑2:esc_url()导致的连锁爆炸

login.php里需要显示SSO登录入口,我写了一个JS函数来构建授权URL:

$sso_url = esc_url($authorize_base . '?app_key=' . $app_key . '&redirect_uri=' . $redirect_uri . '&state=' . $state);

本地测试没问题,一部署到服务器,整个survey站点打不开了。Nginx报502 Bad Gateway,错误日志里写的是ERR_HTTP2_PROTOCOL_ERROR。

我先是怀疑Nginx配置出了问题,改了半天配置还是不行。然后我以为是PHP-FPM的问题,重启了服务,依然不行。最后我仔细看错误日志,发现报错指向login.php的第185行。

我打开login.php第185行,发现是这行代码:$sso_url = esc_url(...)

我愣住了。esc_url()是WordPress的函数,survey里根本没有引入WordPress的函数库!login.php Fatal Error → FastCGI返回错误 → Nginx HTTP2协议错误 → 整个站点打不开。这是一连串的连锁反应。

改成esc_attr()之后,站点恢复正常。事后想想挺后怕的——如果我不是先检查Nginx日志,可能要花更多时间才能定位到问题根源。

坑3:=== 和 == 的类型陷阱

这个问题出现了三次,每次都让我怀疑人生。

survey的get_setting()函数通过json_decode读取配置文件,返回值里"1"可能被解析成integer 1。但我的代码里用严格比较=== '1'来检查SSO是否启用。

$settings = json_decode(file_get_contents($config_file), true);if ($settings['sso_enabled'] === '1') { // 启用SSO}

问题在于,json_decode有时候会把"1"解析成integer,有时候解析成string。严格比较的时候,1 !== '1',所以SSO配置永远读不到"已启用"状态。

这个坑出现在三个地方:login.php的SSO按钮显示、settings.php的配置读取、后台管理界面的checkbox状态。改了三处才彻底修好。

教训:PHP的类型比较要小心,json_decode的输出类型不可控,要么统一用==弱比较,要么在读取后强制类型转换

坑4:esc_attr()把URL变成了噩梦

login.php的JS里输出SSO登录URL时,我用esc_attr()转义了参数:

echo '<a href="' . esc_attr($sso_url) . '">卓影账号登录</a>';

JS拿到URL之后直接当链接用。看起来没问题,但esc_attr()会把&转义成&amp;。所以实际的URL变成了:

https://sjinyu.com/authorize.php?app_key=xxx&redirect_uri=xxx&state=xxx

 

浏览器JS解析这个URL时,把&amp;当成了参数值的一部分,而不是参数分隔符。结果authorize.php只收到了app_key参数,redirect_uri和state全丢了。

报错信息是"Missing required parameters",我对着URL看了半天,愣是没看出来哪里有问题。直到我想起来去查Nginx的访问日志,看实际请求带了什么参数,才发现URL里的&全变成了&amp;

教训:HTML转义函数不能用于URL,URL需要用urlencode()或者rawurlencode()。这个问题看似低级,但偏偏就在我眼皮底下发生了。

坑5:session key不匹配

这个问题比较隐蔽。SSO登录流程走完了,verify.php验证token成功,数据库里也创建了session记录。但用户看到的是"欢迎回来,xxx"——session数据是对的,但页面没有跳转。

我加了N多日志来调试,最后发现:survey的User_Auth类用的是$_SESSION['survey_user_id']$_SESSION['survey_user_token'],而verify.php回调之后写入的是$_SESSION['user_id']

两个key不匹配!session里存了用户ID,但取的时候用的是另一个key,所以永远是空的。

这个坑让我意识到:全局变量的命名一定要有规范,不能想怎么写就怎么写。最后我统一了session key的命名规则,加了统一的前缀sjinyu_来区分不同模块的session数据。

坑6:HTTP2配置的兼容性

这个问题严格来说不是代码bug,而是Nginx配置的坑。

survey站我启用了HTTP2,配置写的是listen 443 ssl http2;(新语法),但某些客户端访问时报ERR_HTTP2_PROTOCOL_ERROR。换成ssl on; listen 443 http2;(旧语法)就好了。

虽然新语法在官方文档里是推荐的,但实际部署中,某些浏览器版本或者某些CDN配置下,旧语法反而更稳定。这个问题没有标准答案,遇到协议级别的错误,有时候就是得多试试不同的配置写法

全站退出:统一才是真正的体验

SSO登录搞通了,但退出是各退各的。

在survey退出之后,sjinyu.com那边还是登录状态。反过来也一样。这个体验很不完整——用户以为退出了,但其实只是退出了当前站点。

我调研了一圈业界做法,最后选择了一个简单有效的方案:退出时显示一个过渡页面,用隐藏的img标签请求其他站点的退出接口。

<img src="https://sjinyu.com/logout.php?sig=xxx&t=xxx" style="display:none" onload="next()" 
onerror="next()"><img src="https://survey.sjinyu.com/logout.php?sig=xxx&t=xxx" style="display:none" 
onload="next()" onerror="next()">

原理是这样的:img标签发请求的时候会带上目标域的cookie,所以能清掉那边的session。两个退出请求发完之后,强制跳转回首页。

为了防止恶意调用,我在请求里加了签名验证参数,只有来源站点才能成功调用退出接口。过渡页面最多等3秒就强制跳转,不会卡住用户。

最后的效果就是:任一站点退出,所有站点都退出。体验终于统一了。

自动感知登录:终极体验的追求

登录搞通了,但我还想更进一步。

用户打开survey的时候,还是要手动点击"卓影账号登录"按钮才能发起SSO流程。能不能更丝滑一点?打开survey就自动检测主站登录状态,已登录就直接跳转?

技术现实是:跨域cookie无法共享。survey.sjinyu.com是子域名,但它和sjinyu.com是不同的域,浏览器不会自动传递cookie。

我做了一个折中方案:

  1. sjinyu.com加了一个check.php接口,支持CORS,查询当前session状态
  2. survey的登录页加载时,JS静默fetch一下这个接口
  3. 已登录 → JS自动跳转到授权页面,用户感觉就是"闪了一下"
  4. 未登录 → 显示正常的登录表单
fetch('https://sjinyu.com/check.php', {credentials: 'include'}) .then(r => r.json()) .then(data => { if (data.logged_in) { // 自动跳转SSO授权 window.location.href = autoRedirectUrl; } });

最终效果:主站已登录的用户打开survey,页面闪一下就自动进入系统了。体验接近无缝。

其实还有一个更彻底的方案:把survey搬到sjinyu.com的子目录下(比如sjinyu.com/survey/),这样就是同域了,cookie自然共享,连跳转都不需要。但survey目前有很多地方硬编码了survey.sjinyu.com的路径,改动太大,暂时搁置了。

总结与展望

从WordPress问卷插件,到独立SaaS平台,再到跨站SSO打通,这一路走下来花了将近一个月。

我踩了无数的坑:Database类找不到、WordPress函数乱用、类型比较踩雷、HTML转义污染URL、session key命名混乱、HTTP2兼容性问题……每一个坑都让我掉了几根头发,但也让我对Web开发的理解更深了一层。

SSO架构我设计成可扩展的,未来如果要打通yingtux.cn影图站,或者新建一个blog.sjinyu.com博客系统,都可以复用这套机制。技术选型不需要最先进,够用就好。自研Token方案比OAuth2简单得多,但安全性不打折

关于AI辅助开发,我有一点感悟:这波AI编程工具真的很强大,帮我快速产出代码、调试逻辑。但AI也会"犯困"——它会下意识地把我项目的类库风格带进去,写出WordPress函数、mysqli语法。AI的代码必须仔细验收,特别是函数引用和类型处理

下一步的计划:survey首页加入自动检测逻辑、yingtux.cn接入SSO体系、以及评估把survey迁移到子目录的可行性。

路还很长,但每一步都算数。

我的微信
微信扫一扫
weinxin
我的微信
微信号已复制
我的微信公众号
微信扫一扫
weinxin
我的公众号
公众号已复制
 
Enjoy
  • 本文由 Enjoy 发表于2026-05-18 01:08:37
  • 转载请务必保留本文链接:https://blog.sjinyu.com/programming/mysso.html
匿名

发表评论

匿名网友
确定

拖动滑块以完成验证