二步验证功能(1/2)

引入totp、qrcode相关依赖
解决一点user上的安全问题
This commit is contained in:
Chenx221 2024-03-05 16:54:24 +08:00
parent f390d80e4f
commit febc732e2d
Signed by: chenx221
GPG Key ID: D7A9EC07024C3021
3 changed files with 104 additions and 98 deletions

View File

@ -44,7 +44,10 @@
"google/recaptcha": "^1.3", "google/recaptcha": "^1.3",
"vlucas/phpdotenv": "^5.6", "vlucas/phpdotenv": "^5.6",
"yiisoft/yii2-httpclient": "^2.0", "yiisoft/yii2-httpclient": "^2.0",
"ipinfo/ipinfo": "^3.1" "ipinfo/ipinfo": "^3.1",
"spomky-labs/otphp": "^11.2",
"ext-gd": "*",
"endroid/qr-code": "^5.0"
}, },
"require-dev": { "require-dev": {
"yiisoft/yii2-debug": "~2.1.0", "yiisoft/yii2-debug": "~2.1.0",

View File

@ -3,12 +3,13 @@
namespace app\controllers; namespace app\controllers;
use app\models\User; use app\models\User;
use app\models\UserSearch;
use app\utils\FileSizeHelper; use app\utils\FileSizeHelper;
use OTPHP\TOTP;
use ReCaptcha\ReCaptcha; use ReCaptcha\ReCaptcha;
use Yii; use Yii;
use yii\base\Exception; use yii\base\Exception;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\filters\AccessControl;
use yii\httpclient\Client; use yii\httpclient\Client;
use yii\web\Controller; use yii\web\Controller;
use yii\web\NotFoundHttpException; use yii\web\NotFoundHttpException;
@ -23,16 +24,41 @@ class UserController extends Controller
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function behaviors() public function behaviors(): array
{ {
return array_merge( return array_merge(
parent::behaviors(), parent::behaviors(),
[ [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'actions' => ['delete', 'info'],
'roles' => ['user'],
],
[
'allow' => true,
'actions' => ['login', 'register'],
'roles' => ['?', '@'], // everyone can access public share
],
[
'allow' => true,
'actions' => ['logout', 'setup-two-factor', 'change-password'],
'roles' => ['@'], // everyone can access public share
]
],
],
'verbs' => [ 'verbs' => [
'class' => VerbFilter::className(), 'class' => VerbFilter::class,
'actions' => [ 'actions' => [
'login' => ['GET', 'POST'],
'logout' => ['GET', 'POST'],
'register' => ['GET', 'POST'],
'delete' => ['POST'], 'delete' => ['POST'],
'info' => ['GET', 'POST'],
'change-password' => ['POST'], 'change-password' => ['POST'],
'setup-two-factor' => ['POST'],
], ],
], ],
] ]
@ -40,93 +66,22 @@ class UserController extends Controller
} }
/** /**
* Lists all User models. * 删除账户(仅自身)
* * @return Response
* @return string
*/ */
public function actionIndex()
{
$searchModel = new UserSearch();
$dataProvider = $searchModel->search($this->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
/**
* Displays a single User model.
* @param int $id ID
* @return string
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}
/**
* Creates a new User model.
* If creation is successful, the browser will be redirected to the 'view' page.
* @return string|Response
*/
public function actionCreate()
{
$model = new User();
if ($this->request->isPost) {
if ($model->load($this->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
} else {
$model->loadDefaultValues();
}
return $this->render('create', [
'model' => $model,
]);
}
/**
* Updates an existing User model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param int $id ID
* @return string|Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($this->request->isPost && $model->load($this->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}
public function actionDelete(): Response public function actionDelete(): Response
{ {
if (Yii::$app->user->isGuest) {
Yii::$app->session->setFlash('error', '滚');
return $this->goHome();
}
$model = Yii::$app->user->identity; $model = Yii::$app->user->identity;
if ($model->deleteAccount()) { if ($model->deleteAccount()) {
Yii::$app->user->logout(); Yii::$app->user->logout();
Yii::$app->session->setFlash('success', 'Account deleted successfully.'); Yii::$app->session->setFlash('success', '账户删除成功');
return $this->redirect(['user/login']);
} else { } else {
Yii::$app->session->setFlash('error', 'Failed to delete account.'); Yii::$app->session->setFlash('error', '账户删除失败');
return $this->redirect(['user/info']);
} }
return $this->redirect(['user/login']);
} }
/** /**
@ -136,7 +91,7 @@ class UserController extends Controller
* @return User the loaded model * @return User the loaded model
* @throws NotFoundHttpException if the model cannot be found * @throws NotFoundHttpException if the model cannot be found
*/ */
protected function findModel($id) protected function findModel(int $id): User
{ {
if (($model = User::findOne(['id' => $id])) !== null) { if (($model = User::findOne(['id' => $id])) !== null) {
return $model; return $model;
@ -150,10 +105,13 @@ class UserController extends Controller
* visit via https://devs.chenx221.cyou:8081/index.php?r=user%2Flogin * visit via https://devs.chenx221.cyou:8081/index.php?r=user%2Flogin
* *
* @return string|Response * @return string|Response
* @throws InvalidConfigException
* @throws \yii\httpclient\Exception
*/ */
public function actionLogin(): Response|string public function actionLogin(): Response|string
{ {
if (!Yii::$app->user->isGuest) { if (!Yii::$app->user->isGuest) {
Yii::$app->session->setFlash('error', '账户已登录,请不要重复登录');
return $this->goHome(); return $this->goHome();
} }
@ -270,10 +228,9 @@ class UserController extends Controller
* Logs out the current user. * Logs out the current user.
* @return Response * @return Response
*/ */
public function actionLogout() public function actionLogout(): Response
{ {
Yii::$app->user->logout(); Yii::$app->user->logout();
return $this->goHome(); return $this->goHome();
} }
@ -285,6 +242,11 @@ class UserController extends Controller
*/ */
public function actionRegister(): Response|string public function actionRegister(): Response|string
{ {
if (!Yii::$app->user->isGuest) {
Yii::$app->session->setFlash('error', '账户已登录,无法进行注册操作');
return $this->goHome();
}
$model = new User(['scenario' => 'register']); $model = new User(['scenario' => 'register']);
if ($model->load(Yii::$app->request->post()) && $model->validate()) { if ($model->load(Yii::$app->request->post()) && $model->validate()) {
// 根据 verifyProvider 的值选择使用哪种验证码服务 // 根据 verifyProvider 的值选择使用哪种验证码服务
@ -314,7 +276,6 @@ class UserController extends Controller
if (!is_dir($userFolder)) { if (!is_dir($userFolder)) {
mkdir($userFolder); mkdir($userFolder);
} }
Yii::$app->session->setFlash('success', 'Registration successful. You can now log in.'); Yii::$app->session->setFlash('success', 'Registration successful. You can now log in.');
return $this->redirect(['login']); return $this->redirect(['login']);
} else { } else {
@ -336,15 +297,18 @@ class UserController extends Controller
*/ */
public function actionInfo(string $focus = null): Response|string public function actionInfo(string $focus = null): Response|string
{ {
if (Yii::$app->user->isGuest) {
Yii::$app->session->setFlash('error', '请先登录');
return $this->redirect(['user/login']);
}
$model = Yii::$app->user->identity; $model = Yii::$app->user->identity;
$usedSpace = FileSizeHelper::getDirectorySize(Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id); $usedSpace = FileSizeHelper::getDirectorySize(Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id);
$vaultUsedSpace = 0; // 保险箱已用空间暂时为0 $vaultUsedSpace = 0; // 保险箱已用空间暂时为0
$storageLimit = $model->storage_limit; $storageLimit = $model->storage_limit;
$totp_secret = null;
$totp_url = null;
if (!$model->is_otp_enabled) {
$totp = TOTP::generate();
$totp_secret = $totp->getSecret();
$totp->setLabel('NetDisk_'.$model->name);
$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())) {
if ($model->save()) { if ($model->save()) {
Yii::$app->session->setFlash('success', '个人简介已更新'); Yii::$app->session->setFlash('success', '个人简介已更新');
@ -354,6 +318,8 @@ class UserController extends Controller
'vaultUsedSpace' => $vaultUsedSpace, // B 'vaultUsedSpace' => $vaultUsedSpace, // B
'storageLimit' => $storageLimit, // MB 'storageLimit' => $storageLimit, // MB
'focus' => 'bio', 'focus' => 'bio',
'totp_secret' => $totp_secret,
'totp_url' => $totp_url,
]); ]);
} }
} }
@ -363,6 +329,8 @@ class UserController extends Controller
'vaultUsedSpace' => $vaultUsedSpace, // B 'vaultUsedSpace' => $vaultUsedSpace, // B
'storageLimit' => $storageLimit, // MB 'storageLimit' => $storageLimit, // MB
'focus' => $focus, 'focus' => $focus,
'totp_secret' => $totp_secret,
'totp_url' => $totp_url,
]); ]);
} }
@ -372,10 +340,6 @@ class UserController extends Controller
*/ */
public function actionChangePassword(): Response|string public function actionChangePassword(): Response|string
{ {
if (Yii::$app->user->isGuest) {
return $this->goHome();
}
$model = Yii::$app->user->identity; $model = Yii::$app->user->identity;
$model->scenario = 'changePassword'; $model->scenario = 'changePassword';
@ -390,5 +354,21 @@ class UserController extends Controller
return $this->redirect(['user/info', 'focus' => 'password']); return $this->redirect(['user/info', 'focus' => 'password']);
} }
/**
* @return string
*/
public function actionSetupTwoFactor(): string
{
$user = Yii::$app->user->identity;
$totp = TOTP::create();
$user->otp_secret = $totp->getSecret();
$user->is_otp_enabled = true;
$user->save(false);
$otpauth = $totp->getProvisioningUri($user->username);
$qrCodeUrl = 'https://api.qrserver.com/v1/create-qr-code/?data=' . urlencode($otpauth);
return $this->render('setup-two-factor', ['qrCodeUrl' => $qrCodeUrl]);
}
} }

View File

@ -5,14 +5,21 @@
/* @var $model app\models\User */ /* @var $model app\models\User */
/* @var $usedSpace int */ /* @var $usedSpace int */
/* @var $vaultUsedSpace int */ /* @var $vaultUsedSpace int */
/* @var $storageLimit int */ /* @var $storageLimit int */
/* @var $focus string */ /* @var $focus string */
/* @var $totp_secret string */
/* @var $totp_url string */
use app\assets\FontAwesomeAsset; use app\assets\FontAwesomeAsset;
use app\utils\FileSizeHelper; use app\utils\FileSizeHelper;
use app\utils\IPLocation; use app\utils\IPLocation;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\RoundBlockSizeMode;
use Endroid\QrCode\Writer\PngWriter;
use yii\bootstrap5\ActiveForm; use yii\bootstrap5\ActiveForm;
use yii\bootstrap5\Html; use yii\bootstrap5\Html;
use yii\bootstrap5\Modal; use yii\bootstrap5\Modal;
@ -35,6 +42,20 @@ $is_unlimited = ($storageLimit === -1); //检查是否为无限制容量
$usedPercent = $is_unlimited ? 0 : round($usedSpace / ($storageLimit * 1024 * 1024) * 100); //网盘已用百分比 $usedPercent = $is_unlimited ? 0 : round($usedSpace / ($storageLimit * 1024 * 1024) * 100); //网盘已用百分比
$vaultUsedPercent = $is_unlimited ? 0 : round($vaultUsedSpace / ($storageLimit * 1024 * 1024) * 100); //保险箱已用百分比 $vaultUsedPercent = $is_unlimited ? 0 : round($vaultUsedSpace / ($storageLimit * 1024 * 1024) * 100); //保险箱已用百分比
$totalUsedPercent = min(($usedPercent + $vaultUsedPercent), 100); //总已用百分比 $totalUsedPercent = min(($usedPercent + $vaultUsedPercent), 100); //总已用百分比
// QR-CODE
if(!is_null($totp_secret)){
$writer = new PngWriter();
$qrCode = QrCode::create($totp_url)
->setEncoding(new Encoding('UTF-8'))
->setErrorCorrectionLevel(ErrorCorrectionLevel::Low)
->setSize(300)
->setMargin(10)
->setRoundBlockSizeMode(RoundBlockSizeMode::Margin)
->setForegroundColor(new Color(0, 0, 0))
->setBackgroundColor(new Color(255, 255, 255));
$result = $writer->write($qrCode);
}
?> ?>
<div class="user-info"> <div class="user-info">
@ -217,6 +238,8 @@ $totalUsedPercent = min(($usedPercent + $vaultUsedPercent), 100); //总已用百
<input class="form-check-input" type="checkbox" role="switch" id="totp-enabled"> <input class="form-check-input" type="checkbox" role="switch" id="totp-enabled">
<label class="form-check-label" for="totp-enabled">启用 TOTP</label> <label class="form-check-label" for="totp-enabled">启用 TOTP</label>
</div> </div>
<!--暂时放在这里-->
<img src="<?= is_null($totp_secret)?'':$result->getDataUri() ?>" alt="qrcode"/>
</div> </div>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
@ -275,11 +298,11 @@ echo Html::tag('div', '确定要删除这个账户?', ['class' => 'modal-body'
echo Html::beginForm(['user/delete'], 'post', ['id' => 'delete-form']); echo Html::beginForm(['user/delete'], 'post', ['id' => 'delete-form']);
echo '<div>'; echo '<div>';
echo Html::checkbox('deleteConfirm', false, ['label' => '确认','id'=>'deleteConfirm']); echo Html::checkbox('deleteConfirm', false, ['label' => '确认', 'id' => 'deleteConfirm']);
echo '</div>'; echo '</div>';
echo '<div class="text-end">'; echo '<div class="text-end">';
echo Html::submitButton('继续删除', ['class' => 'btn btn-danger', 'disabled' => true,'id' => 'deleteButton']); echo Html::submitButton('继续删除', ['class' => 'btn btn-danger', 'disabled' => true, 'id' => 'deleteButton']);
echo '</div>'; echo '</div>';
echo Html::endForm(); echo Html::endForm();