修改邮箱流程
修改邮箱属于账号敏感操作,采用"当前密码 + 新邮箱验证码"双重验证机制:
用户提交修改邮箱请求
↓
┌─────────────────────────────────────┐
│ 步骤 1:密码验证(仅对有密码用户) │
│ - 验证当前登录密码 │
│ - 防止会话劫持后直接改绑邮箱 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 步骤 2:验证码验证 │
│ - 新邮箱必须通过验证码验证 │
│ - 格式校验 + 域名白名单检查 │
│ - 唯一性检查(邮箱未被他人使用) │
│ - 频率限制(防止短信轰炸) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 步骤 3:执行修改 │
│ - 更新 user 表 email 字段 │
│ - 发送通知邮件到旧邮箱 │
└─────────────────────────────────────┘
↓
跳转回安全设置页,显示新邮箱
关键设计点:
密码验证条件判断
// 仅对有密码用户要求验证当前密码
$has_password = !empty($user['password']) || !empty($user['password_hash']);
if ($has_password) {
// 验证当前密码
$verified = user_login_verify($password_current, $user);
if (!$verified) {
message(-1, lang('password_incorrect'));
}
}
// 无密码用户(纯 OAuth 绑定账号)由新邮箱验证码 + 旧邮箱通知兜底
旧邮箱通知机制
// 修改成功后立即通知旧邮箱
$old_email = $user['email'];
$template = xn_email_template('email_change_notify', array(
'sitename' => $conf['sitename'],
'username' => esc_html($user['username']),
'old_email' => $old_email,
'new_email' => $email_new,
'time' => date('Y-m-d H:i:s', $time),
));
xn_send_mail($smtp, $conf['sitename'], $old_email, $template['subject'], $template['body']);
旧邮箱通知的安全意义:
防撞库后静默改绑:如果攻击者通过撞库获取密码,修改邮箱后旧邮箱会收到通知
用户可第一时间察觉:即使账号被劫持,原主人能通过通知邮件发现异常
成本极低:仅增加一封邮件,无额外安全成本
修改密码流程
修改密码采用"旧密码验证 + 新密码强度校验 + 通知邮件"机制:
用户提交修改密码请求
↓
┌─────────────────────────────────────┐
│ 步骤 1:旧密码验证 │
│ - 验证当前密码是否正确 │
│ - 防止会话被劫持后直接修改密码 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 步骤 2:新密码校验 │
│ - 两次输入一致性检查 │
│ - 密码强度策略校验 │
│ · 最小长度(默认 6 位) │
│ - 复杂度要求(数字/大小写/特殊字符)│
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 步骤 3:执行修改 │
│ - 更新密码哈希 │
│ - 清除 Session Token(强制重新登录) │
│ - 发送通知邮件到当前邮箱 │
└─────────────────────────────────────┘
↓
跳转到登录页,要求用户重新登录
关键设计点:
强制重新登录
// 修改成功后清除 token,强制用户重新登录
user_token_clear($uid);
message(0, lang('password_modify_successfully'), array('redirect_url' => url('user-login')));
安全通知邮件
// 通知邮件使用 password_change_notify 模板
$template = xn_email_template('password_change_notify', array(
'sitename' => $conf['sitename'],
'username' => esc_html($user['username']),
'time' => date('Y-m-d H:i:s', $time),
));
强制重新登录的安全意义:
使所有现有会话失效,防止攻击者利用旧会话
用户需用新密码重新登录,确认修改是本人操作
避免"改密后攻击者仍在操作"的风险
表单防重复提交
账号安全设置页面采用统一的防重复提交机制 xnx_form_guard():
function xnx_form_guard(formId, btnId) {
var form = document.getElementById(formId);
var btn = document.getElementById(btnId);
var busy = false;
var originalText = btn.textContent;
// 1. submit capture 阶段同步拦截(最快响应)
form.addEventListener('submit', function(evt) {
if (busy) {
evt.preventDefault();
evt.stopPropagation();
return;
}
busy = true;
btn.disabled = true;
btn.textContent = btn.getAttribute('data-loading-text') || '提交中...';
}, true);
// 2. 请求期间保持禁用状态
form.addEventListener('htmx:before:request', function() {
// 设置 30 秒超时兜底
setTimeout(restore, 30000);
});
// 3. 请求完成后恢复(无论成功/失败)
form.addEventListener('htmx:after:request', restore);
// 4. 网络错误立即恢复
form.addEventListener('htmx:response:error', function() {
restore();
XN.toast('网络请求失败,请重试', 'danger');
});
function restore() {
busy = false;
btn.disabled = false;
btn.textContent = originalText;
}
}
防重机制的四层保障:
层级 | 时机 | 作用 |
|---|---|---|
1 | submit capture 阶段 | 同步拦截快速双击,防止重复提交 |
2 | htmx:before:request | 请求期间禁用按钮,显示 loading 状态 |
3 | htmx:after:request | 请求完成后立即恢复按钮状态 |
4 | htmx:response:error / 30s 超时 | 网络错误或超时时兜底恢复 |
OAuth 无密码用户的特殊处理
纯 OAuth 绑定账号(无密码)在修改邮箱时采用不同的安全策略:
// 判断用户是否有密码
$has_password = !empty($user['password']) || !empty($user['password_hash']);
if ($has_password) {
// 有密码:当前密码 + 新邮箱验证码
// 双重验证,防止会话劫持
} else {
// 无密码:新邮箱验证码 + 旧邮箱通知
// 验证码验证新邮箱归属
// 旧邮箱通知用户修改行为
}
无密码用户的安全兜底:
验证码验证:确保新邮箱归当前用户所有
旧邮箱通知:修改成功后立即通知旧邮箱,用户可第一时间察觉异常
无密码账号补全:登录时强制要求设置密码(见第三章)
邮件模板说明
系统使用以下邮件模板保障账号安全:
模板键 | 用途 | 触发时机 |
|---|---|---|
| 邮箱验证码 | 发送新邮箱验证码时 |
| 改邮箱通知 | 修改邮箱成功后,发送到旧邮箱 |
| 改密通知 | 修改密码成功后,发送到当前邮箱 |
模板变量:
// email_change_code
array('code' => $code, 'sitename' => $sitename)
// email_change_notify
array(
'sitename' => $sitename,
'username' => $username, // 已 esc_html 转义
'old_email' => $old_email,
'new_email' => $new_email,
'time' => $modify_time
)
// password_change_notify
array(
'sitename' => $sitename,
'username' => $username, // 已 esc_html 转义
'time' => $modify_time
)
安全提示:邮件模板中的 username 变量必须使用 esc_html() 转义,防止 HTML 注入攻击。
API 端同步
Web 端和 API 端的安全逻辑保持一致:
端点 | 方法 | 说明 |
|---|---|---|
| GET/POST | Web 端安全设置页 |
| PUT | API 端修改邮箱/密码 |
API 端同样执行:
当前密码验证(有密码用户)
邮箱验证码验证
密码强度校验
旧邮箱/改密通知邮件