下載 Mail.class.php (PHPMailer 輕量封裝) 放入 ThinkPHP/Lib/ORG/。
項目配置 Conf/config.php:
'MAIL_ADDRESS' => 'noreply@example.com', // 發件地址
'MAIL_SMTP' => 'smtp.example.com', // SMTP 服務器
'MAIL_LOGINNAME' => 'noreply@example.com', // 登錄用戶名
'MAIL_PASSWORD' => 'your_smtp_password', // 登錄密碼
'MAIL_CHARSET' => 'UTF-8',
'MAIL_AUTH' => true,
'MAIL_HTML' => true,
<?php
class MailAction extends Action {
public function forget() {
$email = I('post.email');
if (!$email) $this->error('請輸入郵箱');
$user = M('User')->where(['email' => $email])->find();
if (!$user) $this->error('該郵箱未註冊'); // 防止用戶枚舉,實際可統一提示「若存在會發送」
// 生成 Token:用戶ID + 時間戳 + 隨機串 -> SHA256
$token = hash('sha256', $user['id'] . microtime(true) . random_bytes(16));
$expire = time() + 3600; // 1 小時有效
// 存儲到 Session(或 Redis: key=reset_token:$token, value=user_id, ttl=3600)
session('reset_token_' . $token, ['uid' => $user['id'], 'expire' => $expire]);
$resetUrl = U('Mail/reset', ['token' => $token], '', true); // 完整域名 URL
$body = "<p>親愛的 {$user['username']},您申請重置密碼:</p>
<p><a href='$resetUrl'>$resetUrl</a></p>
<p>鏈接 1 小時內有效,若非本人操作請忽略。</p>";
import('@.ORG.Mail');
if (SendMail($email, '密碼重置請求', $body, '系統管理員')) {
$this->success('重置郵件已發送,請查收');
} else {
$this->error('郵件發送失敗,請聯繫管理員');
}
}
public function reset() {
$token = I('get.token');
$data = session('reset_token_' . $token);
if (!$data || $data['expire'] < time()) {
$this->error('鏈接無效或已過期');
}
$this->assign('token', $token);
$this->display(); // 顯示設定新密碼表單
}
public function doReset() {
$token = I('post.token');
$pwd = I('post.password');
$pwd2 = I('post.password2');
$data = session('reset_token_' . $token);
if (!$data || $data['expire'] < time()) $this->error('Token 過期');
if ($pwd !== $pwd2) $this->error('兩次密碼不一致');
if (strlen($pwd) < 8) $this->error('密碼至少 8 位');
$ok = M('User')->where(['id' => $data['uid']])->save(['password' => md5($pwd . C('PWD_SALT'))]);
if ($ok) {
session('reset_token_' . $token, null); // 失效 Token
$this->success('密碼重置成功,請登入', U('Public/login'));
} else {
$this->error('重置失敗,請重試');
}
}
}
Mail/forget.html<form action="__URL__/forget" method="post">
<input type="email" name="email" placeholder="註冊郵箱" required>
<button type="submit">發送重置鏈接</button>
</form>
Mail/reset.html<form action="__URL__/doReset" method="post">
<input type="hidden" name="token" value="{$token}">
<input type="password" name="password" placeholder="新密碼" required>
<input type="password" name="password2" placeholder="確認密碼" required>
<button type="submit">確定修改</button>
</form>
| 風險 | 對策 |
|---|---|
| Token 洩露被盜用 | Token 僅一次性、短效期 (1h)、綁定用戶 ID、HTTPS 傳輸 |
| 用戶枚舉攻擊 | 無論郵箱是否存在,前端統一提示「若郵箱存在,重置郵件已發送」 |
| 密碼明文存儲 | 資料庫存 md5(password . salt) 或 password_hash() (PHP 5.5+) |
| CSRF 攻擊 | 表單加 {:token} 隱藏域,控制器驗證 |
| 暴力破解 Token | Token 使用 256 位熵,配合 Rate Limit (同 IP 15 分鐘最多 3 次請求) |
// Redis 鍵結構
// Key: pwd_reset:{token}
// Value: { "uid": 123, "email": "user@example.com" }
// TTL: 3600 秒
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 存
$redis->setex("pwd_reset:$token", 3600, json_encode(['uid' => $user['id']]));
// 取
$tokenData = json_decode($redis->get("pwd_reset:$token"), true);
if (!$tokenData) { /* 過期或不存在 */ }
// 刪
$redis->del("pwd_reset:$token");
Redis 方案支援分佈式部署、Token 可跨伺服器驗證、不佔用 Session 文件空間。
ThinkPHP 郵件找回密碼的核心在於 Token 生成 → 安全存儲 → 郵件投遞 → 驗證消費 → 失效 閉環。配合 Mail.class.php 發送 HTML 郵件,使用 Session 或 Redis 存儲 Token,嚴格校驗有效期與一次性使用,即可構建安全可靠的密碼重置功能。生產環境務必啟用 SMTP SSL/TLS、配置 SPF/DKIM/DMARC 提高郵件送達率。