适用人群:站长 / 开发者 最后更新:2026-08-06
概述
Xiuno X 的 OAuth 登录安全机制旨在解决第三方登录场景下的账号接管风险。当用户通过 QQ、微信、GitHub、Google 等第三方平台登录时,系统会自动检测账号的密码和邮箱完整性,强制要求用户补全缺失的安全信息,形成"授权登录 → 强制补全 → 完整账号"的安全闭环。
核心目标:防止纯 OAuth 无密码账号被会话劫持后,攻击者直接改绑邮箱、再通过"忘记密码"流程接管账号。
安全威胁模型
风险场景
场景 | 攻击路径 | 危害 |
|---|---|---|
会话劫持 | 窃取 OAuth 登录后的 Session Cookie | 攻击者进入用户账号 |
邮箱改绑 | 劫持会话后修改账号邮箱 | 攻击者控制通信渠道 |
密码重置 | 通过新绑定的邮箱走"忘记密码"流程 | 攻击者设置新密码,永久接管账号 |
防护策略
Plain Text
风险链路:Session 劫持 → 改绑邮箱 → 忘记密码 → 账号接管
↓
防护措施:强制补全密码 → 改绑邮箱需密码验证(已具备)
↑
前提:账号必须有密码核心思路:强制每个 OAuth 用户必须设置本地密码,这样即使会话被劫持,攻击者在改绑邮箱时也需要验证密码,而无法直接走通接管链路。
三层防护体系
第一层:登录后即时拦截
OAuth 回调成功后,系统立即检查账号的密码和邮箱完整性:
Plain Text
用户发起 OAuth 登录
↓
第三方授权回调 → handleCallback()
↓
登录/绑定/合并成功
↓
needsProfileCompletion($uid) 检查
↓
┌─────────────────────────────────────┐
│ 缺密码或缺邮箱? │
│ 是 → 设置 session 标记 │
│ 跳转到 oauth-perfect 补全页 │
│ 否 → 正常跳转个人中心 │
└─────────────────────────────────────┘关键代码位置:
OAuthService::needsProfileCompletion() - 判断逻辑
route/oauth.php callback 分支 - 拦截与跳转
第二层:全局路由强制拦截
即使补全流程被跳过,全局 Hook 也会强制拦截未完成的用户:
PHP
// plugin/xnx_oauth/hook/index_inc_route_before.php
// 在每次请求分发前执行
if (class_exists('OAuthService', false)) {
$_oauth_perfect_uid = intval($uid);
if ($_oauth_perfect_uid > 0 && OAuthService::isOAuthUser($_oauth_perfect_uid)) {
$_oauth_perfect_state = OAuthService::needsProfileCompletion($_oauth_perfect_uid);
if (!$_oauth_perfect_state['complete']) {
// 仅放行:补全页、OAuth 子路由、登录/注册/退出、验证码/附件/API
if (!$_oauth_allow) {
http_location(url('oauth-perfect'));
}
}
}
}放行规则(防止用户被锁死):
oauth全部子路由(补全页自身)user路由下的login/logout/create(登录、退出、注册)captcha/lang/attach/api(资源和 API 路由)
第三层:补全页安全验证
补全页包含完整的安全验证机制:
密码设置
复杂度校验:通过
SecurityConfigService::checkPasswordPolicy()校验密码强度两次输入确认:防止输入错误
防重提交:
xnx_form_guard()防止表单重复提交
邮箱绑定
邮箱格式校验:
filter_var($email, FILTER_VALIDATE_EMAIL)域名白名单:管理员配置允许的邮箱域名
唯一性检查:邮箱未被其他用户使用
验证码验证:通过邮件发送 6 位随机验证码
频率限制:
xn_email_rate_check()+xn_email_rate_record()防止轰炸
核心 API
OAuthService 静态方法
isOAuthUser($uid)
判断用户是否为 OAuth 绑定用户(任一第三方有绑定记录即算)。
PHP
use OAuthService;
// 检查 uid=123 是否为 OAuth 用户
$isOAuth = OAuthService::isOAuthUser(123);
// 返回:bool使用场景:
全局拦截器判断是否需要强制补全
个人中心显示"绑定账号"入口
管理员审计 OAuth 用户比例
needsProfileCompletion($uid)
检查用户是否需要补全账号信息(密码/邮箱)。
PHP
// 获取补全状态
$state = OAuthService::needsProfileCompletion(123);
// 返回:array
// array(
// 'need_password' => bool, // 是否需要设置密码
// 'need_email' => bool, // 是否需要绑定邮箱
// 'complete' => bool, // 是否已完成补全
// )状态说明:
need_password | need_email | complete | 说明 |
|---|---|---|---|
true | true | false | 新 OAuth 用户,需设置密码和邮箱 |
true | false | false | 有邮箱但无密码(罕见) |
false | true | false | 有密码但无邮箱(需绑定邮箱) |
false | false | true | 已完成补全,正常用户 |
路由说明
路由 | 方法 | 说明 |
|---|---|---|
| GET | 渲染补全页 |
| POST | 保存密码和邮箱 |
| POST | 发送邮箱验证码 |
Session 标记
系统使用以下 Session 变量管理补全流程:
Session 键 | 类型 | 说明 |
|---|---|---|
| int | 需要补全的用户 UID |
| string | 当前邮箱验证码 |
| string | 验证码对应的邮箱地址 |
补全页交互流程
Plain Text
┌─────────────────────────────────────────────────────────────┐
│ 完善账号信息 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [头像] │
│ [用户名] │
│ 为保障账号安全,请设置登录密码并绑定邮箱 │
│ │
│ ──────────────────────────────────────────────── │
│ │
│ 新密码: [_________________________] │
│ 确认密码: [_________________________] │
│ │
│ 新邮箱: [_________________________] │
│ 验证码: [___________] [发送验证码] 60s倒计时 │
│ │
│ [保存并继续] │
│ │
│ ──────────────────────────────────────────────── │
│ │
│ [退出登录] │
│ │
└─────────────────────────────────────────────────────────────┘表单防重提交
JavaScript
// 防止快速双击提交
function xnx_form_guard(formId, btnId) {
// 1. capture 阶段同步拦截(最快)
form.addEventListener('submit', function(evt) {
if(busy) { evt.preventDefault(); return; }
busy = true;
btn.disabled = true;
}, true);
// 2. 请求期间禁用按钮 + 显示 loading
form.addEventListener('htmx:before:request', () => { ... });
form.addEventListener('htmx:after:request', restore);
form.addEventListener('htmx:response:error', restore);
// 3. 30 秒超时兜底
setTimeout(restore, 30000);
}验证码发送流程
Plain Text
用户点击"发送验证码"
↓
前端校验邮箱格式
↓
POST oauth-perfect-sendcode
↓
┌─────────────────────────────────┐
│ 1. CsrfService::check() │
│ 2. 检查 need_email 状态 │
│ 3. 邮箱格式验证 │
│ 4. 域名白名单检查 │
│ 5. 邮箱唯一性检查 │
│ 6. 生成 6 位随机验证码 │
│ 7. 存入 Session │
│ 8. 检查频率限制 │
│ 9. 发送邮件 │
│ 10. 返回成功 + 倒计时秒数 │
└─────────────────────────────────┘
↓
前端显示 60 秒倒计时
↓
倒计时结束后可重新发送配置与扩展
密码策略配置
密码复杂度通过 SecurityConfigService 统一管理:
PHP
// 后台路径:系统设置 → 安全 → 账号安全
// 读取当前配置
$minLength = SecurityConfigService::get('security_password_min_length', 6);
$complexity = SecurityConfigService::get('security_password_complexity', 'none');
// 复杂度档位:
// - none: 仅最小长度
// - number: 必须包含数字
// - mixed: 必须包含大小写字母
// - special: 必须包含大小写字母、数字、特殊字符邮箱域名白名单
PHP
// 允许的邮箱域名列表
$allowedDomains = SecurityConfigService::get('security_allowed_email_domains', '');
// 格式:gmail.com,qq.com,163.com
// 空值表示不限制邮件发送配置
OAuth 补全使用与系统相同的邮件发送机制:
SMTP 配置:后台"邮件设置"
邮件模板:
email_change_code(验证码邮件)频率限制:每邮箱每小时最大发送次数
常见问题
Q1:新老 OAuth 用户都会被强制补全吗?
是的。只要是 OAuth 绑定用户(isOAuthUser($uid) 返回 true),且账号缺密码或邮箱,下次登录都会被拦截。这是预期行为,确保所有 OAuth 用户都有完整的账号信息。
Q2:普通注册用户会被强制补全吗?
不会。补全机制仅对 OAuth 用户生效。普通注册用户即使缺邮箱也不会被拦截,因为他们有本地密码作为第二身份验证。
Q3:补全页能跳过吗?
不能。三层防护确保无法绕过:
OAuth 回调成功后直接跳转到补全页
全局路由拦截所有非放行路由
完成前 session 标记持续有效
Q4:补全期间用户能退出登录吗?
可以。补全页底部提供"退出登录"链接,用户可在任何时候退出。
Q5:已有密码的 OAuth 用户需要重新设置密码吗?
不需要。needsProfileCompletion() 只检查是否"缺"密码,已有密码的用户跳过密码设置步骤。
Q6:补全完成后还能解绑所有 OAuth 登录方式吗?
不能。系统会检查是否有其他登录方式:
有密码 → 可解绑所有 OAuth
有其他 OAuth 绑定 → 可解绑当前 provider
无密码且仅有一个 OAuth → 不允许解绑
Q7:邮箱验证码有效期多久?
验证码存储在 Session 中,有效期与 Session 相同(默认 1 小时)。验证成功后立即清除。
Q8:能自定义补全页吗?
可以。补全页模板位于:
Plain Text
plugin/xnx_oauth/view/htm/oauth_perfect.htm可通过模板继承或插件 Hook 修改显示内容,但核心验证逻辑不应修改。
Q9:如何手动触发补全检查?
PHP
// 在任意位置检查用户状态
$state = OAuthService::needsProfileCompletion($uid);
if (!$state['complete']) {
// 跳转到补全页
header('Location: ' . url('oauth-perfect'));
exit;
}Q10:API 登录的 OAuth 用户如何补全?
API 场景下 OAuth 补全通过专用接口处理:
获取当前补全状态:
GET /api/oauth-perfect发送邮箱验证码:
POST /api/oauth-perfect-sendcode提交补全信息:
POST /api/oauth-perfect
相关文件
核心代码
文件 | 说明 |
|---|---|
| OAuth 服务类(含补全判断方法) |
| OAuth 路由处理(含补全分支) |
| 全局强制拦截 Hook |
| 补全页模板 |
配置与语言
文件 | 说明 |
|---|---|
| 插件配置(版本号 1.1.1) |
| 简体中文语言包 |
| 繁体中文语言包 |
| 英文语言包 |
依赖服务
文件 | 说明 |
|---|---|
| 安全配置服务(密码策略) |
| CSRF 令牌验证 |
| 邮件发送函数 |
| 邮件模板配置 |
安全最佳实践
站长配置建议
设置合理的密码复杂度:推荐
mixed或special级别配置邮箱域名白名单:限制为企业域名或常用邮箱服务商
启用邮件频率限制:防止验证码被滥用
定期审计 OAuth 用户:检查是否有异常绑定
插件开发者注意事项
不要绕过全局拦截:自定义 OAuth 插件应复用
OAuthService::needsProfileCompletion()保持 Session 标记同步:自定义补全流程时需维护
oauth_perfect_required使用标准验证函数:密码校验走
SecurityConfigService::checkPasswordPolicy()记录补全日志:补全成功/失败记录到安全日志
版本历史
版本 | 日期 | 变更内容 |
|---|---|---|
1.1.1 | 2026-08-06 | 新增 OAuth 登录后强制补全机制(三层防护) |
1.1.0 | 2026-08-05 | OAuth 2.0 统一登录基础功能 |