[开发]第三方登录(third_party_login)插件说明

👑Lv.11 元老 🌞 骨干会员
2026-10-25 01:14:05

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/third_party_login/说明.md。

第三方登录(third_party_login)插件说明

版本:1.0.0 | 作者:FlintHub | 状态:⛔ 未启用

功能:支持 GitHub / QQ / 微信 / 短信 四种登录方式,后台按 Tab 独立配置;

第三方校验通过但未绑定站内账号时引导「绑定已有账号」(不自动注册),并提供绑定管理与解绑。


一、功能简介

  • 四种登录方式(Plugin::$fields 定义的四个 provider)
provider名称凭据字段协议 / 网关
githubGitHubclient_id / client_secretOAuth 2.0(github.com)
qqQQapp_id / app_keyOAuth 2.0(graph.qq.com)
wechat微信app_id / app_secretOAuth 2.0 扫码(open.weixin.qq.com)
sms短信access_key / access_secret / sign_name / template_code阿里云短信(dysmsapi.aliyuncs.com)

  • 端点固定:三个 OAuth 平台的授权 / 换 token / 取用户信息地址全部硬编码在 Plugin::$endpoints,

不接受自定义网关或自定义 redirect_uri(防开放重定向与钓鱼中转)

  • 独立开关:每个 provider 一个 enabled 开关;isEnabled() 要求「开关打开 且 必填项全部非空」才算启用
  • 登录页按钮注入:layout_head_end 钩子仅在 auth/login 页注入资源,且「零 provider 启用时完全不输出」
  • 绑定已有账号:第三方校验通过但该 openid 未绑定任何站内账号时,引导输入「站内用户名 + 密码」完成绑定。

本插件不做自动注册——没有账号的用户必须先去注册

  • 绑定管理页:/third-party-login/bindings 列出已绑定账号,逐条可解绑
  • 后台管理:/admin/third-party-login 四个 Tab(GitHub / QQ / 微信 / 短信),每 Tab 独立保存,互不影响
  • 零内联:样式与脚本全在 assets/,视图无内联 style 与 on* 事件

二、目录结构

plugins/third_party_login/
├── plugin.json                      # 插件清单(5 个钩子 + admin_url + permissions: route:admin, net:fetch)
├── Plugin.php                       # 主类:建表、配置读写、绑定关系、短信验证码与频率、端点与回调地址
├── FrontController.php              # 前台:OAuth 跳转/回调、短信发送/登录、绑定/解绑、绑定列表
├── AdminController.php              # 后台:设置页读取与保存(四个 Tab 共用)
├── hook/
│   ├── init_after.php               # 幂等建表兜底(缓存标记 data/.tpl_table_cache)
│   ├── route_register.php           # 前台路由注册
│   ├── admin_route_register.php     # 后台路由注册(需 route:admin)
│   ├── layout_head_end.php          # 仅 auth/login 页注入样式/脚本与按钮数据
│   └── nav_user_menu_items.php      # 用户下拉菜单「第三方账号绑定」入口(PC + 移动双触发)
├── views/
│   ├── admin.php                    # 后台设置页(四 Tab,Alpine 切换)
│   ├── bindings.php                 # 绑定管理页(列表 + 解绑)
│   └── oauth_bind.php               # 绑定引导页 / 错误页(phase: oauth | sms | error)
├── assets/
│   ├── style.css                    # 样式(全用 --mn-* 主题变量,零硬编码主题色)
│   └── script.js                    # 登录页按钮渲染、短信发送/登录、后台开关联动(零内联)
├── data/                            # third_party_login.sqlite(独立库)+ .htaccess / web.config
└── lang/                            # zh.php / en.php / zh_tw.php(各 98 键,三语零差异)

三、数据表

插件独立库:plugins/third_party_login/data/third_party_login.sqlite

表用途
tpl_config配置(provider + field 联合主键,value)。每个 provider 的字段与开关都存在这里
tpl_bindings绑定关系(user_id / provider / openid / nickname / avatar / created_at;UNIQUE(provider, openid))
tpl_sms_codes短信验证码(phone / code_hash(sha256)/ expires_at / used / created_at)
tpl_sms_rate短信频率计数(bucket + min_start 联合唯一,cnt)

  • 索引:idx_tpl_bindings_user(user_id)、idx_tpl_sms_codes_phone(phone)
  • 建表:activate() + init_after 双保险(幂等 DDL);init_after 带 .tpl_table_cache 戳文件门控,

表已存在时每请求 0 条 DDL(规范性能红线:单插件每请求所有 hook 查询总和 ≤ 5)

  • 卸载:uninstall() 删四张表。本插件配置全在独立库,不写核心 settings,故无需清理核心键
  • ⚠️ tpl_sms_codes.phone 存明文手机号(code_hash 才是哈希);tpl_bindings.openid 对 sms 用

'ph:' + sha256(手机号),不存明文

四、路由

前台

方法路径说明
GET/third-party-login/login/{provider}跳转第三方授权页(provider ∈ github / qq / wechat)
GET/third-party-login/callback/{provider}授权回调:校验 state → 换 token → 拉用户信息 → 登录或引导绑定
POST/third-party-login/sms/send发送短信验证码(CSRF)
POST/third-party-login/sms/login短信验证码登录(CSRF)
POST/third-party-login/bind绑定已有账号(CSRF)
GET/third-party-login/bindings绑定管理页(需登录)
POST/third-party-login/unbind解绑(需登录 + CSRF)

后台

方法路径说明
GET/admin/third-party-login设置页(`?tab=github\
POST/admin/third-party-login/save保存指定 provider 的配置(CSRF)

⚠️ 后台路由需声明 route:admin;外呼第三方接口需 net:fetch。二者均已声明。

五、核心规则

OAuth 流程(防 CSRF / 防重放)

① 发起(oauthLogin)
   provider 白名单校验 → isEnabled 校验
   → 生成 state = bin2hex(random_bytes(16))(32 位十六进制)
   → 存 $_SESSION['tpl_oauth'] = { state, provider, expires: now + 600 }
   → 302 到平台授权页(client_id / redirect_uri / scope / state 全部 rawurlencode)

② 回调(oauthCallback)
   → 取出并**立即 unset** $_SESSION['tpl_oauth'](一次性)
   → 四重校验:存在 / provider 一致 / state 用 hash_equals 比对 / 未过期(600s)
   → 取 code(缺失即错误页)
   → 换 access_token → 拉 openid / 昵称 / 头像
   → 已绑定 → 直接登录;未绑定 → 已登录则直接绑定,未登录则引导绑定已有账号

  • 任一校验不过 → error_log + 错误页,不泄露失败细节
  • redirect_uri 由 Plugin::callbackUrl($provider) 生成,并对 SITE_URL 做引号/空白/尾斜杠清洗

(防 FLINTHUB_SITE_URL 被误写成带引号的整串 → 回调地址变成 https://域名%22%22/...)

短信验证码

环节规则
手机号正则 ^1[3-9]\d{9}$(中国大陆手机号)
验证码6 位数字,random_int(100000, 999999)(避免弱随机)
存储只存 sha256(code),不存明文
有效期SMS_TTL = 300 秒(5 分钟)
使用单次使用:校验通过立即置 used = 1;置位用 UPDATE ... WHERE id = ? AND used = 0 + rowCount() 断言,并发下只有一次成功
作废旧码每次发新码前把该手机号的旧码全部置 used = 1
频率限制双维度:ip:<REMOTE_ADDR> 与 ph:<手机号> 各自每分钟 ≤ 1 次(防短信轰炸)
计数清理每次限流检查顺带删除 1 小时前的旧记录,控制表增长

登录动作(与核心明文登录保持一致)

FrontController::loginUser() 按顺序执行四件事,缺一不可:

① session_regenerate_id(true)      // 防会话固定
② $_SESSION['user_id'] = $userId
③ Csrf::rotate()                   // 轮换 CSRF token
④ (new User())->updateLoginTime()  // 更新最后登录时间
⑤ Plugin::hook('auth_login_after', [user_id, username])   // 通知其他插件

绑定 / 解绑

  • 绑定:tpl_bindings 上 UNIQUE(provider, openid) 是硬约束;写入用 ON CONFLICT(provider, openid) DO UPDATE
  • 绑定已有账号(bind())必须先过「用户名 + password_verify 密码」校验;

并触发核心 auth_login_check 钩子,读 $GLOBALS['mod_system_login_blocked'] 判断封禁 → 被禁用户不允许绑定

  • 解绑:按 (user_id, provider, openid) 三元组删除,只能解绑自己的(user_id 取自当前会话)
  • ⚠️ 同一第三方账号(openid)只能绑定一个站内账号;重复绑定会改绑到后者(见注意事项)

后台配置保存

  • 只接受该 provider 白名单内的字段(Plugin::$fields[$provider]),其余字段一律忽略
  • 每个值 mb_substr(..., 0, 191) 截断;enabled 归一为 '0' / '1'
  • 每 Tab 独立保存:只写当前 provider 的键,不影响其他 Tab
  • 保存后 302 回当前 Tab(?tab=<provider>),不会跳回第一个

六、注意事项

  • ⚠️ 依赖 net:fetch 权限:OAuth 换 token / 拉用户信息 / 阿里云短信发送均走 \app\Helpers\Plugin::http()。

permission_enforce 为强制模式(默认)时,未声明该权限会被 DENY

  • ⚠️ 回调地址必须在第三方平台原样登记:GitHub / QQ / 微信后台填的 redirect_uri 必须与

Plugin::callbackUrl() 输出逐字符一致(含 http/https、端口、结尾无斜杠)。后台设置页已直接展示该地址

  • ⚠️ SITE_URL 不要写成带引号的整串。callbackUrl() 会清洗首尾引号/空白/尾斜杠,但源头写对更稳妥
  • ⚠️ 手机号明文存库:tpl_sms_codes.phone 是明文。若对隐私有更高要求,需另行加密改造
  • ⚠️ 重复绑定会改绑:tpl_bindings 唯一键是 (provider, openid) 而非 (user_id, provider),

因此同一第三方账号被第二个站内账号绑定时,会把归属改到后者。正常流程(先 findBinding 命中即登录)不会触发,

但管理员排查「绑定关系变了」时应知悉此语义

  • ⚠️ 解绑不设「至少保留一种登录方式」保护:用户可解绑全部第三方账号,之后靠站内密码登录。

本插件不做自动注册,因此不存在「无密码账号」

  • ⚠️ 建表缓存标记在 data/.tpl_table_cache:手工删表后需一并删该标记,否则不会重建
  • ⚠️ 本插件目录名含下划线,核心 Plugin::activate/deactivate/uninstall() 的类名推导

("\\Plugin\\{$name}\\Plugin")会拼出 \Plugin\third_party_login\Plugin 而无法命中

\Plugin\ThirdPartyLogin\Plugin → 已在 Plugin.php 末尾用 class_alias 补别名

(否则卸载时四张数据表不会被删除)

  • ⚠️ nav_user_menu_items 是双触发钩子(PC + 移动各一次,带 placement 参数)。

本插件两次都输出(与 qa / user_level 同款),与 post_favorite 不同——后者会显式跳过 pc

  • ⚠️ 登录页资源只走 layout_head_end(因为 auth/login 是核心页面,无法用插件视图的 section('css'));

后台页与绑定页则走视图 section('css')。改动注入条件时注意别把两套混淆

  • 当前状态:四张表均无业务数据(插件未启用);三语各 98 键,键集合零差异、键序一致
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 2 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录