totp前端设计(2/2)

二步验证功能(2/2)
*测试时发现了其他用户无法正常访问所有功能,这个问题下次再说
This commit is contained in:
Chenx221 2024-03-07 20:35:50 +08:00
parent 63bc49d63e
commit e944d3c781
Signed by: chenx221
GPG Key ID: D7A9EC07024C3021
5 changed files with 202 additions and 50 deletions

View File

@ -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,15 +138,22 @@ 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)) {
$user->last_login = date('Y-m-d H:i:s'); // 如果用户启用了二步验证,将用户重定向到二步验证页面
$user->last_login_ip = Yii::$app->request->userIP; if ($user->is_otp_enabled) {
if ($user->save(false)) { Yii::$app->session->set('login_verification', ['id' => $user->id]);
return $this->goBack(); return $this->redirect(['user/verify-two-factor']);
} else { } else {
Yii::$app->session->setFlash('error', '登陆成功,但出现了内部错误'); //login without 2FA
$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);
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
@ -306,7 +353,7 @@ class UserController extends Controller
if (!$model->is_otp_enabled) { if (!$model->is_otp_enabled) {
$totp = TOTP::generate(); $totp = TOTP::generate();
$totp_secret = $totp->getSecret(); $totp_secret = $totp->getSecret();
$totp->setLabel('NetDisk_'.$model->name); $totp->setLabel('NetDisk_' . $model->name);
$totp_url = $totp->getProvisioningUri(); $totp_url = $totp->getProvisioningUri();
} }
if (Yii::$app->request->isPost && $model->load(Yii::$app->request->post())) { if (Yii::$app->request->isPost && $model->load(Yii::$app->request->post())) {
@ -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())) {
$recoveryCodes = $this->generateRecoveryCodes(); $totp_secret = $user->otp_secret;
$totp_input = $user->totp_input;
// 保存恢复代码到数据库或其他安全的地方 $otp = TOTP::createFromSecret($totp_secret);
if ($otp->verify($totp_input)) {
// 显示恢复代码给用户 $recoveryCodes = $this->generateRecoveryCodes();
Yii::$app->session->setFlash('success', '二步验证已启用。您的恢复代码是:' . implode(', ', $recoveryCodes)); $user->is_otp_enabled = 1;
$user->recovery_codes = implode(',', $recoveryCodes);
// ...其他代码... $user->save();
Yii::$app->session->setFlash('success', '二步验证已启用');
} 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']);
}
}
} }

View File

@ -58,8 +58,8 @@ 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'],
[['username', 'password', 'email', 'password2'], 'required', 'on' => 'register'], [['username', 'password', 'email', 'password2'], 'required', 'on' => 'register'],

View File

@ -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) {
'method' => 'post' $form = ActiveForm::begin([
]); ?> 'action' => ['user/setup-two-factor'],
'method' => 'post'
<?= Html::activeHiddenInput($user, 'totp_secret', ['value' => $totp_secret]) ?> ]);
<?= $form->field($user, 'totp_input')->textInput()->label('最后一步! 输入TOTP应用程序上显示的密码以启用二步验证') ?> echo Html::activeHiddenInput($user, 'otp_secret', ['value' => $totp_secret]);
<?= Html::submitButton('启用二步验证', ['class' => 'btn btn-primary']) ?> echo $form->field($user, 'totp_input')->textInput()->label('最后一步! 输入TOTP应用程序上显示的密码以启用二步验证');
echo Html::submitButton('启用二步验证', ['class' => 'btn btn-primary']);
<?php ActiveForm::end(); ?> ActiveForm::end();
}
?>
</div> </div>
</div> </div>
<?php <?php

View 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 -->

View File

@ -1,9 +1,22 @@
$(document).ready(function() { $(document).ready(function () {
$('#deleteConfirm').change(function() { $('#deleteConfirm').change(function () {
if(this.checked) { if (this.checked) {
$('#deleteButton').prop('disabled', false); $('#deleteButton').prop('disabled', false);
} else { } else {
$('#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);
});
}); });