1.增加谷歌验证器
This commit is contained in:
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use Throwable;
|
||||
use support\think\Db;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
use app\common\facade\Token;
|
||||
use app\admin\model\AdminLog;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\Admin as AdminModel;
|
||||
use support\Response;
|
||||
@@ -17,7 +19,7 @@ class Admin extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['create_time', 'update_time', 'password', 'salt', 'login_failure', 'last_login_time', 'last_login_ip', 'channel_id'];
|
||||
protected array|string $preExcludeFields = ['create_time', 'update_time', 'password', 'salt', 'login_failure', 'last_login_time', 'last_login_ip', 'channel_id', 'totp_secret', 'totp_bind_time'];
|
||||
|
||||
protected array|string $quickSearchField = ['username', 'nickname'];
|
||||
|
||||
@@ -224,6 +226,50 @@ class Admin extends Backend
|
||||
]);
|
||||
}
|
||||
|
||||
public function resetTotp(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]);
|
||||
}
|
||||
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
$id = (int) ($request->post('id') ?? 0);
|
||||
if ($id <= 0) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
if ($id === $this->auth->id) {
|
||||
return $this->error(__('Cannot reset your own authenticator, please modify in database'));
|
||||
}
|
||||
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Reset Google Authenticator'));
|
||||
|
||||
$row->save([
|
||||
'totp_secret' => '',
|
||||
'totp_bind_time' => null,
|
||||
]);
|
||||
|
||||
Token::clear(\app\admin\library\Auth::TOKEN_TYPE, $id);
|
||||
Token::clear(\app\admin\library\Auth::TOKEN_TYPE . '-refresh', $id);
|
||||
|
||||
return $this->success(__('Google Authenticator reset successfully'));
|
||||
}
|
||||
|
||||
public function del(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
|
||||
@@ -13,8 +13,8 @@ class AdminInfo extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['username', 'last_login_time', 'password', 'salt', 'status', 'channel_id'];
|
||||
protected array $authAllowFields = ['id', 'username', 'nickname', 'avatar', 'email', 'mobile', 'motto', 'last_login_time'];
|
||||
protected array|string $preExcludeFields = ['username', 'last_login_time', 'password', 'salt', 'status', 'channel_id', 'totp_secret', 'totp_bind_time'];
|
||||
protected array $authAllowFields = ['id', 'username', 'nickname', 'avatar', 'email', 'mobile', 'motto', 'last_login_time', 'totp_bound'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
|
||||
@@ -110,4 +110,16 @@ return [
|
||||
'Rejected successfully' => 'Rejected successfully',
|
||||
'Success' => 'success',
|
||||
'Failed' => 'failed',
|
||||
'Please enter Google Authenticator code' => 'Please enter Google Authenticator code',
|
||||
'Please bind Google Authenticator' => 'Please bind Google Authenticator',
|
||||
'TOTP session expired, please login again' => 'Verification session expired, please login again',
|
||||
'Google Authenticator already bound' => 'Google Authenticator already bound',
|
||||
'Google Authenticator code error' => 'Google Authenticator code error',
|
||||
'Google Authenticator bind failed' => 'Google Authenticator bind failed',
|
||||
'Google Authenticator bound successfully' => 'Google Authenticator bound successfully',
|
||||
'Google Authenticator not bound' => 'Google Authenticator not bound',
|
||||
'Reset Google Authenticator' => 'Reset Google Authenticator',
|
||||
'Google Authenticator reset successfully' => 'Google Authenticator reset. The admin must re-bind on next login',
|
||||
'Cannot reset your own authenticator, please modify in database' => 'Cannot reset your own authenticator. Super admin recovery requires database change',
|
||||
'Login failed' => 'Login failed',
|
||||
];
|
||||
@@ -129,4 +129,16 @@ return [
|
||||
'Rejected successfully' => '驳回成功',
|
||||
'Success' => '成功',
|
||||
'Failed' => '失败',
|
||||
'Please enter Google Authenticator code' => '请输入谷歌验证器验证码',
|
||||
'Please bind Google Authenticator' => '请绑定谷歌验证器',
|
||||
'TOTP session expired, please login again' => '验证会话已过期,请重新登录',
|
||||
'Google Authenticator already bound' => '谷歌验证器已绑定',
|
||||
'Google Authenticator code error' => '谷歌验证器验证码错误',
|
||||
'Google Authenticator bind failed' => '谷歌验证器绑定失败',
|
||||
'Google Authenticator bound successfully' => '谷歌验证器绑定成功',
|
||||
'Google Authenticator not bound' => '谷歌验证器未绑定',
|
||||
'Reset Google Authenticator' => '重置谷歌验证器',
|
||||
'Google Authenticator reset successfully' => '谷歌验证器已重置,该管理员下次登录需重新绑定',
|
||||
'Cannot reset your own authenticator, please modify in database' => '不能重置自己的验证器,超管丢失验证器请在数据库中修改',
|
||||
'Login failed' => '登录失败',
|
||||
];
|
||||
@@ -24,6 +24,8 @@ class Auth extends \ba\Auth
|
||||
public const LOGIN_RESPONSE_CODE = 303;
|
||||
public const NEED_LOGIN = 'need login';
|
||||
public const LOGGED_IN = 'logged in';
|
||||
public const NEED_TOTP = 'need_totp';
|
||||
public const NEED_BIND_TOTP = 'need_bind_totp';
|
||||
public const TOKEN_TYPE = 'admin';
|
||||
|
||||
protected bool $loginEd = false;
|
||||
@@ -90,6 +92,21 @@ class Auth extends \ba\Auth
|
||||
}
|
||||
|
||||
public function login(string $username, string $password, bool $keep = false): bool
|
||||
{
|
||||
if (!$this->verifyCredentials($username, $password)) {
|
||||
return false;
|
||||
}
|
||||
if (config('buildadmin.admin_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
if ($keep) {
|
||||
$this->setRefreshToken($this->refreshTokenKeepTime);
|
||||
}
|
||||
return $this->loginSuccessful();
|
||||
}
|
||||
|
||||
public function verifyCredentials(string $username, string $password): bool
|
||||
{
|
||||
$this->model = Admin::where('username', $username)->find();
|
||||
if (!$this->model) {
|
||||
@@ -124,18 +141,44 @@ class Auth extends \ba\Auth
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hasTotpBound(): bool
|
||||
{
|
||||
if (!$this->model) {
|
||||
return false;
|
||||
}
|
||||
return \app\common\library\AdminTotp::isBound($this->model->getData('totp_secret'));
|
||||
}
|
||||
|
||||
public function loadAdminById(int $adminId): bool
|
||||
{
|
||||
$this->model = Admin::where('id', $adminId)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
if ($this->model->status === 'disable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function finalizeLogin(bool $keep = false): bool
|
||||
{
|
||||
if (!$this->model) {
|
||||
return false;
|
||||
}
|
||||
if (config('buildadmin.admin_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
|
||||
if ($keep) {
|
||||
$this->setRefreshToken($this->refreshTokenKeepTime);
|
||||
}
|
||||
if (!$this->loginSuccessful()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return $this->loginSuccessful();
|
||||
}
|
||||
|
||||
public function setRefreshToken(int $keepTime = 0): void
|
||||
|
||||
@@ -21,6 +21,8 @@ use support\think\Db;
|
||||
* @property string $password 密码密文
|
||||
* @property string $salt 密码盐
|
||||
* @property string $status 状态:enable=启用,disable=禁用
|
||||
* @property string $totp_secret TOTP密钥(加密)
|
||||
* @property int $totp_bind_time TOTP绑定时间
|
||||
*/
|
||||
class Admin extends Model
|
||||
{
|
||||
@@ -43,8 +45,18 @@ class Admin extends Model
|
||||
protected array $append = [
|
||||
'group_arr',
|
||||
'group_name_arr',
|
||||
'totp_bound',
|
||||
];
|
||||
|
||||
protected array $hidden = [
|
||||
'totp_secret',
|
||||
];
|
||||
|
||||
public function getTotpBoundAttr($value, $row): bool
|
||||
{
|
||||
return \app\common\library\AdminTotp::isBound($row['totp_secret'] ?? '');
|
||||
}
|
||||
|
||||
public function getGroupArrAttr($value, $row): array
|
||||
{
|
||||
return Db::name('admin_group_access')
|
||||
|
||||
Reference in New Issue
Block a user