totp前端设计(2/2)
二步验证功能(2/2) *测试时发现了其他用户无法正常访问所有功能,这个问题下次再说
This commit is contained in:
parent
63bc49d63e
commit
e944d3c781
@ -14,6 +14,7 @@ use yii\httpclient\Client;
|
|||||||
use yii\web\Controller;
|
use yii\web\Controller;
|
||||||
use yii\web\NotFoundHttpException;
|
use yii\web\NotFoundHttpException;
|
||||||
use yii\filters\VerbFilter;
|
use yii\filters\VerbFilter;
|
||||||
|
use yii\web\RangeNotSatisfiableHttpException;
|
||||||
use yii\web\Response;
|
use yii\web\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,17 +36,17 @@ class UserController extends Controller
|
|||||||
[
|
[
|
||||||
'allow' => true,
|
'allow' => true,
|
||||||
'actions' => ['delete', 'info'],
|
'actions' => ['delete', 'info'],
|
||||||
'roles' => ['user'],
|
'roles' => ['user'], // only user can do these
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'allow' => true,
|
'allow' => true,
|
||||||
'actions' => ['login', 'register'],
|
'actions' => ['login', 'register','verify-two-factor'],
|
||||||
'roles' => ['?', '@'], // everyone can access public share
|
'roles' => ['?', '@'], // everyone can access public share
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'allow' => true,
|
'allow' => true,
|
||||||
'actions' => ['logout', 'setup-two-factor', 'change-password'],
|
'actions' => ['logout', 'setup-two-factor', 'change-password', 'download-recovery-codes', 'remove-two-factor'],
|
||||||
'roles' => ['@'], // everyone can access public share
|
'roles' => ['@'], // only logged-in user can do these
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -59,6 +60,9 @@ class UserController extends Controller
|
|||||||
'info' => ['GET', 'POST'],
|
'info' => ['GET', 'POST'],
|
||||||
'change-password' => ['POST'],
|
'change-password' => ['POST'],
|
||||||
'setup-two-factor' => ['POST'],
|
'setup-two-factor' => ['POST'],
|
||||||
|
'download-recovery-codes' => ['GET'],
|
||||||
|
'remove-two-factor' => ['POST'],
|
||||||
|
'verify-two-factor' => ['GET','POST'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
@ -134,16 +138,23 @@ class UserController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (($captchaResponse !== null && $isCaptchaValid) || ($verifyProvider === 'None')) {
|
if (($captchaResponse !== null && $isCaptchaValid) || ($verifyProvider === 'None')) {
|
||||||
if ($model->login()) {
|
// 验证用户名和密码
|
||||||
//login success
|
$user = User::findOne(['username' => $model->username]);
|
||||||
$user = Yii::$app->user->identity;
|
if ($user !== null && $user->validatePassword($model->password)) {
|
||||||
|
// 如果用户启用了二步验证,将用户重定向到二步验证页面
|
||||||
|
if ($user->is_otp_enabled) {
|
||||||
|
Yii::$app->session->set('login_verification', ['id' => $user->id]);
|
||||||
|
return $this->redirect(['user/verify-two-factor']);
|
||||||
|
} else {
|
||||||
|
//login without 2FA
|
||||||
$user->last_login = date('Y-m-d H:i:s');
|
$user->last_login = date('Y-m-d H:i:s');
|
||||||
$user->last_login_ip = Yii::$app->request->userIP;
|
$user->last_login_ip = Yii::$app->request->userIP;
|
||||||
if ($user->save(false)) {
|
if (!$user->save(false)){
|
||||||
return $this->goBack();
|
|
||||||
} else {
|
|
||||||
Yii::$app->session->setFlash('error', '登陆成功,但出现了内部错误');
|
Yii::$app->session->setFlash('error', '登陆成功,但出现了内部错误');
|
||||||
}
|
}
|
||||||
|
Yii::$app->user->login($user, $model->rememberMe ? 3600 * 24 * 30 : 0);
|
||||||
|
return $this->goHome();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Yii::$app->session->setFlash('error', '用户名密码错误或账户已禁用');
|
Yii::$app->session->setFlash('error', '用户名密码错误或账户已禁用');
|
||||||
}
|
}
|
||||||
@ -156,6 +167,41 @@ class UserController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Response|string
|
||||||
|
*/
|
||||||
|
public function actionVerifyTwoFactor(): Response|string
|
||||||
|
{
|
||||||
|
if (!Yii::$app->session->has('login_verification')) {
|
||||||
|
Yii::$app->session->setFlash('error', '非法访问');
|
||||||
|
return $this->goHome();
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = new User();
|
||||||
|
$user = User::findOne(Yii::$app->session->get('login_verification')['id']);
|
||||||
|
|
||||||
|
if ($model->load(Yii::$app->request->post())) {
|
||||||
|
// 验证二步验证代码
|
||||||
|
$otp = TOTP::createFromSecret($user->otp_secret);
|
||||||
|
if ($otp->verify($model->totp_input)) {
|
||||||
|
$user->last_login = date('Y-m-d H:i:s');
|
||||||
|
$user->last_login_ip = Yii::$app->request->userIP;
|
||||||
|
if (!$user->save(false)){
|
||||||
|
Yii::$app->session->setFlash('error', '登陆成功,但出现了内部错误');
|
||||||
|
}
|
||||||
|
Yii::$app->user->login($user, $model->rememberMe ? 3600 * 24 * 30 : 0);
|
||||||
|
Yii::$app->session->remove('login_verification');
|
||||||
|
return $this->goHome();
|
||||||
|
} else {
|
||||||
|
Yii::$app->session->setFlash('error', '二步验证代码错误');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('verifyTwoFactor', [
|
||||||
|
'model' => $model,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证 reCAPTCHA 的响应
|
* 验证 reCAPTCHA 的响应
|
||||||
* 无法保证这项服务在中国大陆的可用性
|
* 无法保证这项服务在中国大陆的可用性
|
||||||
@ -293,6 +339,7 @@ class UserController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param string|null $focus
|
||||||
* @return string|Response
|
* @return string|Response
|
||||||
*/
|
*/
|
||||||
public function actionInfo(string $focus = null): Response|string
|
public function actionInfo(string $focus = null): Response|string
|
||||||
@ -320,6 +367,7 @@ class UserController extends Controller
|
|||||||
'focus' => 'bio',
|
'focus' => 'bio',
|
||||||
'totp_secret' => $totp_secret,
|
'totp_secret' => $totp_secret,
|
||||||
'totp_url' => $totp_url,
|
'totp_url' => $totp_url,
|
||||||
|
'is_otp_enabled' => $model->is_otp_enabled
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,10 +379,12 @@ class UserController extends Controller
|
|||||||
'focus' => $focus,
|
'focus' => $focus,
|
||||||
'totp_secret' => $totp_secret,
|
'totp_secret' => $totp_secret,
|
||||||
'totp_url' => $totp_url,
|
'totp_url' => $totp_url,
|
||||||
|
'is_otp_enabled' => $model->is_otp_enabled == 1
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 更改密码
|
||||||
* @return Response|string
|
* @return Response|string
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@ -354,28 +404,88 @@ class UserController extends Controller
|
|||||||
return $this->redirect(['user/info', 'focus' => 'password']);
|
return $this->redirect(['user/info', 'focus' => 'password']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function actionSetupTwoFactor()
|
/**
|
||||||
|
* 启用二步验证
|
||||||
|
* @return Response
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function actionSetupTwoFactor(): Response
|
||||||
{
|
{
|
||||||
// ...其他代码...
|
$user = Yii::$app->user->identity;
|
||||||
|
|
||||||
// 生成恢复代码
|
if ($user->load(Yii::$app->request->post())) {
|
||||||
|
$totp_secret = $user->otp_secret;
|
||||||
|
$totp_input = $user->totp_input;
|
||||||
|
|
||||||
|
$otp = TOTP::createFromSecret($totp_secret);
|
||||||
|
if ($otp->verify($totp_input)) {
|
||||||
$recoveryCodes = $this->generateRecoveryCodes();
|
$recoveryCodes = $this->generateRecoveryCodes();
|
||||||
|
$user->is_otp_enabled = 1;
|
||||||
// 保存恢复代码到数据库或其他安全的地方
|
$user->recovery_codes = implode(',', $recoveryCodes);
|
||||||
|
$user->save();
|
||||||
// 显示恢复代码给用户
|
Yii::$app->session->setFlash('success', '二步验证已启用');
|
||||||
Yii::$app->session->setFlash('success', '二步验证已启用。您的恢复代码是:' . implode(', ', $recoveryCodes));
|
} else {
|
||||||
|
Yii::$app->session->setFlash('error', '二步验证启用失败,请重新添加');
|
||||||
// ...其他代码...
|
}
|
||||||
|
}
|
||||||
|
return $this->redirect(['user/info']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateRecoveryCodes($length = 10, $numCodes = 10)
|
/**
|
||||||
|
* 移除二步验证
|
||||||
|
*/
|
||||||
|
public function actionRemoveTwoFactor(): void
|
||||||
|
{
|
||||||
|
$user = Yii::$app->user->identity;
|
||||||
|
if ($user->is_otp_enabled) {
|
||||||
|
$user->otp_secret = null;
|
||||||
|
$user->is_otp_enabled = 0;
|
||||||
|
$user->recovery_codes = null;
|
||||||
|
$user->save();
|
||||||
|
Yii::$app->session->setFlash('success', '二步验证已关闭');
|
||||||
|
} else {
|
||||||
|
Yii::$app->session->setFlash('error', '二步验证未启用,无需关闭');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成10组随机的恢复代码
|
||||||
|
* @return array
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function generateRecoveryCodes(): array
|
||||||
{
|
{
|
||||||
$codes = [];
|
$codes = [];
|
||||||
for ($i = 0; $i < $numCodes; $i++) {
|
for ($i = 0; $i < 10; $i++) {
|
||||||
$codes[] = Yii::$app->security->generateRandomString($length);
|
$codes[] = Yii::$app->security->generateRandomString(10);
|
||||||
}
|
}
|
||||||
return $codes;
|
return $codes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取恢复代码(以txt文本的形式提供)
|
||||||
|
* @return Response|\yii\console\Response
|
||||||
|
* @throws RangeNotSatisfiableHttpException
|
||||||
|
*/
|
||||||
|
public function actionDownloadRecoveryCodes(): Response|\yii\console\Response
|
||||||
|
{
|
||||||
|
// 获取当前登录的用户模型
|
||||||
|
$user = Yii::$app->user->identity;
|
||||||
|
|
||||||
|
// 检查用户是否启用了 TOTP
|
||||||
|
if ($user->is_otp_enabled) {
|
||||||
|
// 获取恢复代码
|
||||||
|
$recoveryCodesString = implode("\n", explode(',', $user->recovery_codes));
|
||||||
|
// 发送恢复代码给用户
|
||||||
|
return Yii::$app->response->sendContentAsFile(
|
||||||
|
$recoveryCodesString,
|
||||||
|
'recovery_codes.txt',
|
||||||
|
['mimeType' => 'text/plain']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 如果用户没有启用 TOTP,返回一个错误消息
|
||||||
|
Yii::$app->session->setFlash('error', '获取失败,您还没有启用二步验证。');
|
||||||
|
return $this->redirect(['user/info']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ class User extends ActiveRecord implements IdentityInterface
|
|||||||
return [
|
return [
|
||||||
[['status', 'is_encryption_enabled', 'is_otp_enabled'], 'integer'],
|
[['status', 'is_encryption_enabled', 'is_otp_enabled'], 'integer'],
|
||||||
[['created_at', 'last_login'], 'safe'],
|
[['created_at', 'last_login'], 'safe'],
|
||||||
[['bio'], 'string'],
|
[['bio', 'totp_input'], 'string'],
|
||||||
[['encryption_key', 'otp_secret', 'recovery_codes'], 'string', 'max' => 255],
|
[['encryption_key', 'otp_secret', 'recovery_codes'], 'string', 'max' => 255],
|
||||||
[['last_login_ip'], 'string', 'max' => 45],
|
[['last_login_ip'], 'string', 'max' => 45],
|
||||||
[['username', 'password'], 'required', 'on' => 'login'],
|
[['username', 'password'], 'required', 'on' => 'login'],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
//这个页面仿照Windows 11设置中的账户页面设计
|
//这个页面部分仿照Windows 11设置中的账户页面设计
|
||||||
|
|
||||||
/* @var $this yii\web\View */
|
/* @var $this yii\web\View */
|
||||||
/* @var $model app\models\User */
|
/* @var $model app\models\User */
|
||||||
@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
/* @var $totp_url string */
|
/* @var $totp_url string */
|
||||||
|
|
||||||
|
/* @var $is_otp_enabled bool */
|
||||||
|
|
||||||
use app\assets\FontAwesomeAsset;
|
use app\assets\FontAwesomeAsset;
|
||||||
use app\models\User;
|
use app\models\User;
|
||||||
use app\utils\FileSizeHelper;
|
use app\utils\FileSizeHelper;
|
||||||
@ -239,13 +241,11 @@ $user = new User();
|
|||||||
</h5>
|
</h5>
|
||||||
<div>
|
<div>
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="totp-enabled"
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
data-bs-toggle="modal"
|
id="totp-enabled" <?= $is_otp_enabled ? 'checked' : '' ?>>
|
||||||
data-bs-target="#totpSetupModal">
|
|
||||||
<label class="form-check-label" for="totp-enabled" data-bs-toggle="modal"
|
<label class="form-check-label" for="totp-enabled" data-bs-toggle="modal"
|
||||||
data-bs-target="#totpSetupModal">启用 TOTP</label>
|
data-bs-target="#totpSetupModal">启用 TOTP</label>
|
||||||
</div>
|
</div>
|
||||||
<!--暂时放在这里-->
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
@ -254,9 +254,7 @@ $user = new User();
|
|||||||
备用码
|
备用码
|
||||||
</h5>
|
</h5>
|
||||||
<div>
|
<div>
|
||||||
<button id="generate-backup-codes" class="btn btn-outline-primary btn-sm">
|
<?= Html::a('获取恢复代码(请妥善保存)', Url::to(['user/download-recovery-codes']), ['class' => 'btn btn-outline-primary btn-sm', 'id' => 'generate-backup-codes']) ?>
|
||||||
生成备用码
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -333,7 +331,8 @@ Modal::begin([
|
|||||||
<li>
|
<li>
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US">Google
|
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US">Google
|
||||||
Authenticator</a></li>
|
Authenticator</a></li>
|
||||||
<li><a href="https://play.google.com/store/apps/details?id=com.azure.authenticator&hl=en_US">Microsoft Authenticator</a></li>
|
<li><a href="https://play.google.com/store/apps/details?id=com.azure.authenticator&hl=en_US">Microsoft
|
||||||
|
Authenticator</a></li>
|
||||||
<li><a href="https://play.google.com/store/apps/details?id=com.authy.authy&hl=en">Authy</a></li>
|
<li><a href="https://play.google.com/store/apps/details?id=com.authy.authy&hl=en">Authy</a></li>
|
||||||
<!-- Add more applications as needed -->
|
<!-- Add more applications as needed -->
|
||||||
</ul>
|
</ul>
|
||||||
@ -344,16 +343,18 @@ Modal::begin([
|
|||||||
onclick="navigator.clipboard.writeText('<?= $totp_secret ?>')">Copy
|
onclick="navigator.clipboard.writeText('<?= $totp_secret ?>')">Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<?php $form = ActiveForm::begin([
|
<?php
|
||||||
'action' => ['user/actionSetupTwoFactor'],
|
if (!$is_otp_enabled) {
|
||||||
|
$form = ActiveForm::begin([
|
||||||
|
'action' => ['user/setup-two-factor'],
|
||||||
'method' => 'post'
|
'method' => 'post'
|
||||||
]); ?>
|
]);
|
||||||
|
echo Html::activeHiddenInput($user, 'otp_secret', ['value' => $totp_secret]);
|
||||||
<?= Html::activeHiddenInput($user, 'totp_secret', ['value' => $totp_secret]) ?>
|
echo $form->field($user, 'totp_input')->textInput()->label('最后一步! 输入TOTP应用程序上显示的密码以启用二步验证');
|
||||||
<?= $form->field($user, 'totp_input')->textInput()->label('最后一步! 输入TOTP应用程序上显示的密码以启用二步验证') ?>
|
echo Html::submitButton('启用二步验证', ['class' => 'btn btn-primary']);
|
||||||
<?= Html::submitButton('启用二步验证', ['class' => 'btn btn-primary']) ?>
|
ActiveForm::end();
|
||||||
|
}
|
||||||
<?php ActiveForm::end(); ?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
|
28
views/user/verifyTwoFactor.php
Normal file
28
views/user/verifyTwoFactor.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use yii\bootstrap5\Html;
|
||||||
|
use yii\bootstrap5\ActiveForm;
|
||||||
|
|
||||||
|
/** @var yii\web\View $this */
|
||||||
|
/** @var app\models\User $model */
|
||||||
|
|
||||||
|
$this->title = '2FA验证';
|
||||||
|
$this->params['breadcrumbs'][] = $this->title;
|
||||||
|
?>
|
||||||
|
<div class="user-login-2fa">
|
||||||
|
<h1><?= Html::encode($this->title) ?></h1>
|
||||||
|
|
||||||
|
<p>请在下方输入二步验证代码:</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<?php $form = ActiveForm::begin(); ?>
|
||||||
|
|
||||||
|
<?= $form->field($model, 'totp_input')->passwordInput()->label('二步验证代码') ?>
|
||||||
|
<div class="form-group">
|
||||||
|
<?= Html::submitButton('提交', ['class' => 'btn btn-primary']) ?>
|
||||||
|
</div>
|
||||||
|
<?php ActiveForm::end(); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- user-login-2fa -->
|
@ -6,4 +6,17 @@ $(document).ready(function() {
|
|||||||
$('#deleteButton').prop('disabled', true);
|
$('#deleteButton').prop('disabled', true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$('#totp-enabled').change(function () {
|
||||||
|
if (this.checked) {
|
||||||
|
$('#totpSetupModal').modal('show');
|
||||||
|
}else {
|
||||||
|
$.post('index.php?r=user%2Fremove-two-factor', function () {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#totpSetupModal').on('hidden.bs.modal', function () {
|
||||||
|
$('#totp-enabled').prop('checked', false);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
Loading…
Reference in New Issue
Block a user