feat: 添加清除结果功能到检查提供者

refactor: 更新URL哈希处理逻辑

feat: 添加聊天消息存储支持

docs: 更新API控制器基类文档

chore: 删除无用脚本文件

fix: 修复分类模型返回类型问题

feat: 添加回执登录功能

build: 更新依赖项配置

style: 统一HTML模板中的哈希ID引用格式

ci: 添加部署和检查脚本
This commit is contained in:
Developer
2026-04-30 10:19:56 +08:00
parent 847ebc8501
commit 00ff5f152a
588 changed files with 87168 additions and 14961 deletions

View File

@@ -3,6 +3,7 @@
namespace app\api\controller;
use app\common\controller\Api;
use app\common\library\Receipt;
use app\common\library\Ems;
use app\common\library\Sms;
use fast\Random;
@@ -12,24 +13,26 @@ use think\Validate;
/**
* 用户安全接口
* @time 2026-04-29
* @description 用户安全相关API含注册/登录/改密/改邮箱/重置密码等
* @lastUpdate v7.7.0 从User控制器拆分引入邮箱验证机制
* @name UserSecurity
* @description 用户安全相关API含注册/登录/改密/改邮箱/重置密码/回执登录等
* @lastUpdate v8.0.0 去除邮箱验证码依赖引入HMAC-SHA256回执验证机制新增receiptLogin
*/
class UserSecurity extends Api
{
protected $noNeedLogin = ['login', 'mobilelogin', 'register', 'resetpwd', 'tokenLogin', 'sendEms', 'checkEms'];
protected $noNeedLogin = ['login', 'mobilelogin', 'register', 'resetpwd', 'tokenLogin', 'receiptLogin', 'sendEms', 'checkEms'];
protected $noNeedRight = '*';
private static $rateLimitKey = 'api_rate_limit:';
private static $rateLimits = [
'login' => ['max' => 20, 'window' => 300],
'register' => ['max' => 10, 'window' => 3600],
'resetpwd' => ['max' => 5, 'window' => 3600],
'changepwd' => ['max' => 10, 'window' => 3600],
'changeemail' => ['max' => 10, 'window' => 3600],
'changemobile'=> ['max' => 10, 'window' => 3600],
'sendEms' => ['max' => 10, 'window' => 300],
'tokenLogin' => ['max' => 30, 'window' => 300],
'login' => ['max' => 50, 'window' => 300],
'register' => ['max' => 30, 'window' => 3600],
'resetpwd' => ['max' => 20, 'window' => 3600],
'changepwd' => ['max' => 30, 'window' => 3600],
'changeemail' => ['max' => 30, 'window' => 3600],
'changemobile' => ['max' => 30, 'window' => 3600],
'sendEms' => ['max' => 30, 'window' => 300],
'tokenLogin' => ['max' => 80, 'window' => 300],
'receiptLogin' => ['max' => 50, 'window' => 300],
];
private static $testMode = false;
@@ -115,29 +118,20 @@ class UserSecurity extends Api
return $user && isset($user['is_test']) && $user['is_test'] == 1;
}
private function isTestEmail($email)
private function verifyReceipt($action, $payloadStr)
{
if (!self::$testMode) {
return false;
$receipt = $this->request->post('receipt', '', 'trim');
$sig = $this->request->post('sig', '', 'trim');
$result = Receipt::verify($receipt, $sig, $action, $payloadStr);
if (!$result['valid']) {
$this->error($result['msg']);
}
$testEmails = Config::get('fastadmin.test_emails') ?: [];
if (is_string($testEmails)) {
$testEmails = explode(',', $testEmails);
}
return in_array($email, $testEmails);
}
private function verifyEmsCode($email, $code, $event)
{
if (self::$testMode && $code === '888888') {
return true;
}
return Ems::check($email, $code, $event);
return true;
}
/**
* @name 账号密码登录
* @desc 支持用户名/邮箱+密码登录
* @desc 支持用户名/邮箱+密码登录,无需回执
*/
public function login()
{
@@ -159,6 +153,54 @@ class UserSecurity extends Api
}
}
/**
* @name 回执登录
* @desc 客户端验证邮箱/手机后,用回执替代密码登录
* @since v8.0.0
*/
public function receiptLogin()
{
$this->checkRateLimit('receiptLogin');
$account = $this->request->post('account', '', 'trim');
$receipt = $this->request->post('receipt', '', 'trim');
$sig = $this->request->post('sig', '', 'trim');
if (!$account || !$receipt || !$sig) {
$this->error(__('Invalid parameters'));
}
$this->validateLength($account, '账号', 2, 100);
$this->detectMaliciousInput($account);
$result = Receipt::verifyFlexible($receipt, $sig, 'receipt_login', $account);
if (!$result['valid']) {
$this->error($result['msg']);
}
$user = null;
if (Validate::is($account, "email")) {
$user = \app\common\model\User::getByEmail($account);
} elseif (Validate::regex($account, "^1\d{10}$")) {
$user = \app\common\model\User::getByMobile($account);
} else {
$user = \app\common\model\User::getByUsername($account);
}
if (!$user) {
$this->error(__('User not found'));
}
if ($user->status != 'normal') {
$this->error(__('Account is locked'));
}
$ret = $this->auth->direct($user->id);
if ($ret) {
$data = ['userinfo' => $this->auth->getUserinfo()];
$this->success(__('Logged in successful'), $data);
} else {
$this->error($this->auth->getError());
}
}
/**
* @name 手机号登录
* @desc 手机号+短信验证码登录,未注册自动注册
@@ -233,7 +275,7 @@ class UserSecurity extends Api
/**
* @name 用户注册
* @desc 注册新用户,邮箱必填且需邮箱验证码
* @desc 注册新用户,需回执验证(客户端已验证邮箱),不再需要邮箱验证码
*/
public function register()
{
@@ -241,7 +283,6 @@ class UserSecurity extends Api
$username = $this->request->post('username', '', 'trim');
$password = $this->request->post('password', '', 'trim');
$email = $this->request->post('email', '', 'trim');
$emailCode = $this->request->post('email_code', '', 'trim');
$mobile = $this->request->post('mobile', '', 'trim');
$mobileCode = $this->request->post('mobile_code', '', 'trim');
@@ -263,13 +304,7 @@ class UserSecurity extends Api
$this->error('该邮箱已被注册');
}
if (!$emailCode) {
$this->error('邮箱验证码为必填项');
}
$this->validateLength($emailCode, '邮箱验证码', 4, 6);
if (!$this->verifyEmsCode($email, $emailCode, 'register')) {
$this->error('邮箱验证码不正确');
}
$this->verifyReceipt('register', $email);
if ($mobile) {
if (!Validate::regex($mobile, "^1\d{10}$")) {
@@ -284,7 +319,6 @@ class UserSecurity extends Api
$ret = $this->auth->register($username, $password, $email, $mobile, []);
if ($ret) {
Ems::flush($email, 'register');
$userId = $this->auth->id;
$verification = db('user')->where('id', $userId)->value('verification');
$verification = $verification ? json_decode($verification, true) : [];
@@ -319,7 +353,7 @@ class UserSecurity extends Api
/**
* @name 修改密码
* @desc 修改密码需验证旧密码+邮箱验证码(双重验证)
* @desc 修改密码需验证旧密码+回执验证(客户端已验证邮箱)
*/
public function changepwd()
{
@@ -327,7 +361,6 @@ class UserSecurity extends Api
$user = $this->auth->getUser();
$oldpassword = $this->request->post('oldpassword', '', 'trim');
$newpassword = $this->request->post('newpassword', '', 'trim');
$emailCode = $this->request->post('email_code', '', 'trim');
if (!$oldpassword || !$newpassword) {
$this->error(__('Invalid parameters'));
@@ -335,32 +368,16 @@ class UserSecurity extends Api
$this->validateLength($oldpassword, '旧密码', 6, 30);
$this->validateLength($newpassword, '新密码', 6, 30);
if ($this->isTestUser($user->id)) {
$ret = $this->auth->changepwd($newpassword, $oldpassword);
if ($ret) {
$this->success(__('Change password successful'));
} else {
$this->error($this->auth->getError());
if (!$this->isTestUser($user->id)) {
$email = $user->email;
if (!$email) {
$this->error('请先绑定邮箱后再修改密码');
}
return;
}
$email = $user->email;
if (!$email) {
$this->error('请先绑定邮箱后再修改密码');
}
if (!$emailCode) {
$this->error('邮箱验证码为必填项');
}
$this->validateLength($emailCode, '邮箱验证码', 4, 6);
if (!$this->verifyEmsCode($email, $emailCode, 'changepwd')) {
$this->error('邮箱验证码不正确');
$this->verifyReceipt('changepwd', $user->username);
}
$ret = $this->auth->changepwd($newpassword, $oldpassword);
if ($ret) {
Ems::flush($email, 'changepwd');
$this->success(__('Change password successful'));
} else {
$this->error($this->auth->getError());
@@ -369,7 +386,7 @@ class UserSecurity extends Api
/**
* @name 重置密码
* @desc 通过邮箱/手机验证重置密码(无需登录)
* @desc 通过回执验证重置密码(无需登录),客户端已验证邮箱/手机
*/
public function resetpwd()
{
@@ -378,13 +395,11 @@ class UserSecurity extends Api
$mobile = $this->request->post("mobile", '', 'trim');
$email = $this->request->post("email", '', 'trim');
$newpassword = $this->request->post("newpassword", '', 'trim');
$captcha = $this->request->post("captcha", '', 'trim');
if (!$newpassword || !$captcha) {
if (!$newpassword) {
$this->error(__('Invalid parameters'));
}
$this->validateLength($newpassword, '新密码', 6, 30);
$this->validateLength($captcha, '验证码', 4, 6);
if ($type == 'mobile') {
if (!Validate::regex($mobile, "^1\d{10}$")) {
@@ -394,10 +409,7 @@ class UserSecurity extends Api
if (!$user) {
$this->error(__('User not found'));
}
if (!Sms::check($mobile, $captcha, 'resetpwd')) {
$this->error(__('Captcha is incorrect'));
}
Sms::flush($mobile, 'resetpwd');
$this->verifyReceipt('resetpwd', $mobile);
} else {
if (!Validate::is($email, "email")) {
$this->error(__('Email is incorrect'));
@@ -406,10 +418,7 @@ class UserSecurity extends Api
if (!$user) {
$this->error(__('User not found'));
}
if (!$this->verifyEmsCode($email, $captcha, 'resetpwd')) {
$this->error('邮箱验证码不正确');
}
Ems::flush($email, 'resetpwd');
$this->verifyReceipt('resetpwd', $email);
}
$this->auth->direct($user->id);
@@ -423,90 +432,70 @@ class UserSecurity extends Api
/**
* @name 修改邮箱
* @desc 修改邮箱需新邮箱验证码验证
* @desc 修改邮箱需回执验证(客户端已验证新邮箱)
*/
public function changeemail()
{
$this->checkRateLimit('changeemail');
$user = $this->auth->getUser();
$email = $this->request->post('email', '', 'trim');
$captcha = $this->request->post('captcha', '', 'trim');
if (!$email || !$captcha) {
if (!$email) {
$this->error(__('Invalid parameters'));
}
if (!Validate::is($email, "email")) {
$this->error(__('Email is incorrect'));
}
$this->validateLength($email, '邮箱', 5, 100);
$this->validateLength($captcha, '验证码', 4, 6);
if (\app\common\model\User::where('email', $email)->where('id', '<>', $user->id)->find()) {
$this->error(__('Email already exists'));
}
if (!$this->verifyEmsCode($email, $captcha, 'changeemail')) {
$this->error(__('Captcha is incorrect'));
}
$this->verifyReceipt('changeemail', $email);
$verification = $user->verification;
$verification->email = 1;
$user->verification = $verification;
$user->email = $email;
$user->save();
Ems::flush($email, 'changeemail');
$this->success();
}
/**
* @name 修改手机号
* @desc 修改手机号需短信验证码验证
* @desc 修改手机号需回执验证(客户端已验证新手机号)
*/
public function changemobile()
{
$this->checkRateLimit('changemobile');
$user = $this->auth->getUser();
$mobile = $this->request->post('mobile', '', 'trim');
$captcha = $this->request->post('captcha', '', 'trim');
if (!$mobile || !$captcha) {
if (!$mobile) {
$this->error(__('Invalid parameters'));
}
if (!Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('Mobile is incorrect'));
}
$this->validateLength($captcha, '验证码', 4, 6);
if (\app\common\model\User::where('mobile', $mobile)->where('id', '<>', $user->id)->find()) {
$this->error(__('Mobile already exists'));
}
if (self::$testMode && $captcha === '888888') {
$result = true;
} else {
$result = false;
try {
$result = Sms::check($mobile, $captcha, 'changemobile');
} catch (\Exception $e) {
$this->error('短信验证服务暂不可用');
}
}
if (!$result) {
$this->error(__('Captcha is incorrect'));
}
$this->verifyReceipt('changemobile', $mobile);
$verification = $user->verification;
$verification->mobile = 1;
$user->verification = $verification;
$user->mobile = $mobile;
$user->save();
try { Sms::flush($mobile, 'changemobile'); } catch (\Exception $e) {}
$this->success();
}
/**
* @name 发送邮箱验证码
* @desc 发送邮箱验证码,支持多种事件类型
* @name 发送邮箱验证码(保留兼容)
* @desc 保留接口兼容旧客户端,新客户端建议使用回执验证
*/
public function sendEms()
{
@@ -549,8 +538,8 @@ class UserSecurity extends Api
}
/**
* @name 校验邮箱验证码
* @desc 校验邮箱验证码是否正确
* @name 校验邮箱验证码(保留兼容)
* @desc 保留接口兼容旧客户端
*/
public function checkEms()
{
@@ -562,7 +551,11 @@ class UserSecurity extends Api
$this->error(__('Invalid parameters'));
}
$ret = $this->verifyEmsCode($email, $captcha, $event);
if (self::$testMode && $captcha === '888888') {
$this->success(__('验证码正确'));
}
$ret = Ems::check($email, $captcha, $event);
if ($ret) {
$this->success(__('验证码正确'));
} else {