1.增加谷歌验证器

This commit is contained in:
2026-06-24 16:25:31 +08:00
parent 5187330c38
commit 3e88a5dd91
21 changed files with 822 additions and 46 deletions

View File

@@ -6,8 +6,10 @@ namespace app\admin\controller;
use ba\ClickCaptcha;
use ba\Random;
use app\admin\model\Admin;
use app\common\facade\Token;
use app\admin\model\AdminLog;
use app\common\library\AdminTotp;
use app\common\controller\Backend;
use support\validation\Validator;
use support\validation\ValidationException;
@@ -16,7 +18,7 @@ use support\Response;
class Index extends Backend
{
protected array $noNeedLogin = ['logout', 'login'];
protected array $noNeedLogin = ['logout', 'login', 'totpVerify', 'totpBindInit', 'totpBindConfirm'];
protected array $noNeedPermission = ['index'];
public function index(Request $request): Response
@@ -102,26 +104,26 @@ class Index extends Backend
AdminLog::instance($request)->setTitle(__('Login'));
$res = $this->auth->login($username, $password, (bool) $keep);
if ($res === true) {
$userInfo = $this->auth->getInfo();
$adminId = $this->auth->id;
$keepTime = (int) config('buildadmin.admin_token_keep_time', 86400 * 3);
// 兜底:若 getInfo 未返回 token在控制器层生成并入库login 成功时必有 adminId
if (empty($userInfo['token']) && $adminId) {
$userInfo['token'] = Random::uuid();
Token::set($userInfo['token'], \app\admin\library\Auth::TOKEN_TYPE, $adminId, $keepTime);
}
if (empty($userInfo['refresh_token']) && $keep && $adminId) {
$userInfo['refresh_token'] = Random::uuid();
Token::set($userInfo['refresh_token'], \app\admin\library\Auth::TOKEN_TYPE . '-refresh', $adminId, 2592000);
}
return $this->success(__('Login succeeded!'), [
'userInfo' => $userInfo
if (!$this->auth->verifyCredentials($username, $password)) {
$msg = $this->auth->getError();
return $this->error($msg ?: __('Incorrect user name or password!'));
}
if ($this->auth->hasTotpBound()) {
$tempToken = AdminTotp::createPendingToken($this->auth->id, AdminTotp::TOKEN_TYPE_VERIFY);
return $this->success(__('Please enter Google Authenticator code'), [
'type' => $this->auth::NEED_TOTP,
'tempToken' => $tempToken,
'username' => $this->auth->username,
]);
}
$msg = $this->auth->getError();
return $this->error($msg ?: __('Incorrect user name or password!'));
$tempToken = AdminTotp::createPendingToken($this->auth->id, AdminTotp::TOKEN_TYPE_BIND);
return $this->success(__('Please bind Google Authenticator'), [
'type' => $this->auth::NEED_BIND_TOTP,
'tempToken' => $tempToken,
'username' => $this->auth->username,
]);
}
return $this->success('', [
@@ -129,6 +131,138 @@ class Index extends Backend
]);
}
public function totpBindInit(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($request->method() !== 'POST') {
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
}
$tempToken = (string) $request->post('tempToken', '');
$adminId = AdminTotp::resolvePendingToken($tempToken, AdminTotp::TOKEN_TYPE_BIND);
if ($adminId <= 0) {
return $this->error(__('TOTP session expired, please login again'));
}
if (!$this->auth->loadAdminById($adminId)) {
return $this->error($this->auth->getError());
}
if ($this->auth->hasTotpBound()) {
return $this->error(__('Google Authenticator already bound'));
}
$secret = AdminTotp::generateSecret();
$label = $this->auth->username;
$qrCode = AdminTotp::getQrDataUri($label, $secret);
return $this->success('', [
'secret' => $secret,
'qrCode' => $qrCode,
'username' => $label,
]);
}
public function totpBindConfirm(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($request->method() !== 'POST') {
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
}
$tempToken = (string) $request->post('tempToken', '');
$secret = (string) $request->post('secret', '');
$code = (string) $request->post('code', '');
$keep = (bool) $request->post('keep');
if ($tempToken === '' || $secret === '' || $code === '') {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$adminId = AdminTotp::resolvePendingToken($tempToken, AdminTotp::TOKEN_TYPE_BIND);
if ($adminId <= 0) {
return $this->error(__('TOTP session expired, please login again'));
}
if (!AdminTotp::verifyCode($secret, $code)) {
return $this->error(__('Google Authenticator code error'));
}
if (!$this->auth->loadAdminById($adminId)) {
return $this->error($this->auth->getError());
}
if ($this->auth->hasTotpBound()) {
AdminTotp::deletePendingToken($tempToken);
return $this->error(__('Google Authenticator already bound'));
}
$encrypted = AdminTotp::encryptSecret($secret);
if ($encrypted === '') {
return $this->error(__('Google Authenticator bind failed'));
}
Admin::where('id', $adminId)->update([
'totp_secret' => $encrypted,
'totp_bind_time' => time(),
]);
AdminTotp::deletePendingToken($tempToken);
if (!$this->auth->finalizeLogin($keep)) {
return $this->error($this->auth->getError() ?: __('Google Authenticator bind failed'));
}
return $this->buildLoginSuccessResponse($keep, __('Google Authenticator bound successfully'));
}
public function totpVerify(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($request->method() !== 'POST') {
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
}
$tempToken = (string) $request->post('tempToken', '');
$code = (string) $request->post('code', '');
$keep = (bool) $request->post('keep');
if ($tempToken === '' || $code === '') {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$adminId = AdminTotp::resolvePendingToken($tempToken, AdminTotp::TOKEN_TYPE_VERIFY);
if ($adminId <= 0) {
return $this->error(__('TOTP session expired, please login again'));
}
if (!$this->auth->loadAdminById($adminId)) {
return $this->error($this->auth->getError());
}
if (!$this->auth->hasTotpBound()) {
AdminTotp::deletePendingToken($tempToken);
return $this->error(__('Google Authenticator not bound'));
}
$encrypted = $this->auth->getAdmin()->getData('totp_secret');
if (!AdminTotp::verifyStoredCode($encrypted, $code)) {
$this->auth->loginFailed();
return $this->error(__('Google Authenticator code error'));
}
AdminTotp::deletePendingToken($tempToken);
if (!$this->auth->finalizeLogin($keep)) {
return $this->error($this->auth->getError() ?: __('Login failed'));
}
return $this->buildLoginSuccessResponse($keep);
}
public function logout(Request $request): Response
{
$response = $this->initializeBackend($request);
@@ -144,4 +278,22 @@ class Index extends Backend
}
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
}
private function buildLoginSuccessResponse(bool $keep, ?string $message = null): Response
{
$userInfo = $this->auth->getInfo();
$adminId = $this->auth->id;
$keepTime = (int) config('buildadmin.admin_token_keep_time', 86400 * 3);
if (empty($userInfo['token']) && $adminId) {
$userInfo['token'] = Random::uuid();
Token::set($userInfo['token'], \app\admin\library\Auth::TOKEN_TYPE, $adminId, $keepTime);
}
if (empty($userInfo['refresh_token']) && $keep && $adminId) {
$userInfo['refresh_token'] = Random::uuid();
Token::set($userInfo['refresh_token'], \app\admin\library\Auth::TOKEN_TYPE . '-refresh', $adminId, 2592000);
}
return $this->success($message ?: __('Login succeeded!'), [
'userInfo' => $userInfo,
]);
}
}