Web Authn(3/3)
修改登录逻辑,实现WebAuthn验证
This commit is contained in:
parent
043e70ab7d
commit
07caef2555
@ -16,7 +16,6 @@ use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAssertionResponseValidator;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponseValidator;
|
||||
use Webauthn\CeremonyStep\CeremonyStepManager;
|
||||
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
|
||||
use Webauthn\Denormalizer\WebauthnSerializerFactory;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
@ -63,12 +62,12 @@ class UserController extends Controller
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => ['login', 'register', 'verify-two-factor'],
|
||||
'actions' => ['login', 'register', 'verify-two-factor', 'verify-assertion', 'request-assertion-options'],
|
||||
'roles' => ['?', '@'], // everyone can access public share
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => ['logout', 'setup-two-factor', 'change-password', 'download-recovery-codes', 'remove-two-factor', 'set-theme', 'change-name', 'create-credential-options', 'create-credential', 'request-assertion-options', 'verify-assertion', 'credential-list', 'credential-delete'],
|
||||
'actions' => ['logout', 'setup-two-factor', 'change-password', 'download-recovery-codes', 'remove-two-factor', 'set-theme', 'change-name', 'create-credential-options', 'create-credential', 'credential-list', 'credential-delete'],
|
||||
'roles' => ['@'], // only logged-in user can do these ( admin included )
|
||||
]
|
||||
],
|
||||
@ -174,14 +173,16 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the login page.
|
||||
* visit via https://devs.chenx221.cyou:8081/index.php?r=user%2Flogin
|
||||
* 显示登录页并接收登录请求
|
||||
* GET时显示登录页,如果带有username参数则直接到输入密码页
|
||||
* POST时验证用户名密码,如果用户启用了二步验证则重定向到二步验证页面
|
||||
*
|
||||
* @param string|null $username
|
||||
* @return string|Response
|
||||
* @throws InvalidConfigException
|
||||
* @throws \yii\httpclient\Exception
|
||||
*/
|
||||
public function actionLogin(): Response|string
|
||||
public function actionLogin(string $username = null): Response|string
|
||||
{
|
||||
if (!Yii::$app->user->isGuest) {
|
||||
Yii::$app->session->setFlash('error', '账户已登录,请不要重复登录');
|
||||
@ -190,36 +191,69 @@ class UserController extends Controller
|
||||
|
||||
$model = new User(['scenario' => 'login']);
|
||||
|
||||
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
|
||||
// 根据 verifyProvider 的值选择使用哪种验证码服务
|
||||
list($verifyProvider, $captchaResponse, $isCaptchaValid) = $this->checkCaptcha();
|
||||
|
||||
if (($captchaResponse !== null && $isCaptchaValid) || ($verifyProvider === 'None')) {
|
||||
// 验证用户名和密码
|
||||
if (($model->load(Yii::$app->request->post()) && $model->validate()) | $username !== null) {
|
||||
if ($model->password === null) {
|
||||
if ($username !== null) {
|
||||
$model->username = $username;
|
||||
}
|
||||
$user = User::findOne(['username' => $model->username]);
|
||||
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_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 {
|
||||
Yii::$app->session->setFlash('error', '用户名密码错误或账户已禁用');
|
||||
if ($user === null) {
|
||||
Yii::$app->session->setFlash('error', '用户不存在');
|
||||
return $this->render('login', [
|
||||
'model' => $model,
|
||||
]);
|
||||
} elseif ($user->status === 0) {
|
||||
Yii::$app->session->setFlash('error', '用户已停用,请联系管理员获取支持');
|
||||
return $this->render('login', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
|
||||
$fidoCredentials = $publicKeyCredentialSourceRepository->findAllForUserEntity($user);
|
||||
if (empty($fidoCredentials) | $username !== null) { //未设置FIDO
|
||||
return $this->render('_login1', [
|
||||
'model' => $model,
|
||||
]);
|
||||
} else { //已设置FIDO
|
||||
return $this->render('_login2', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
Yii::$app->session->setFlash('error', '请等待验证码加载并完成验证');
|
||||
// 根据 verifyProvider 的值选择使用哪种验证码服务
|
||||
list($verifyProvider, $captchaResponse, $isCaptchaValid) = $this->checkCaptcha();
|
||||
|
||||
if (($captchaResponse !== null && $isCaptchaValid) || ($verifyProvider === 'None')) {
|
||||
// 验证用户名和密码
|
||||
$user = User::findOne(['username' => $model->username]);
|
||||
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_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 {
|
||||
Yii::$app->session->setFlash('error', '用户名密码错误或账户已禁用');
|
||||
}
|
||||
} else {
|
||||
Yii::$app->session->setFlash('error', '请等待验证码加载并完成验证');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return $this->render('login', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
return $this->render('login', [
|
||||
return $this->render('_login1', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
@ -703,13 +737,24 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* 请求验证选项
|
||||
* @param string|null $username
|
||||
* @return Response
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function actionRequestAssertionOptions(): Response
|
||||
public function actionRequestAssertionOptions(string $username = null): Response
|
||||
{
|
||||
$user = Yii::$app->user->identity;
|
||||
|
||||
$user = null;
|
||||
if ($username !== null) {
|
||||
$user = User::findOne(['username' => $username]);
|
||||
if ($user === null) {
|
||||
return $this->asJson(['message' => 'User not found']);
|
||||
}
|
||||
} else {
|
||||
$user = Yii::$app->user->identity;
|
||||
}
|
||||
if ($user === null) {
|
||||
return $this->asJson(['message' => 'Guest? Sure?']);
|
||||
}
|
||||
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
|
||||
$registeredAuthenticators = $publicKeyCredentialSourceRepository->findAllForUserEntity($user);
|
||||
|
||||
@ -736,10 +781,12 @@ class UserController extends Controller
|
||||
/**
|
||||
* 验证断言
|
||||
* 用于已登录情况下验证fifo设置是否成功
|
||||
* @param int $is_login
|
||||
* @param int $remember
|
||||
* @return Response
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function actionVerifyAssertion(): Response
|
||||
public function actionVerifyAssertion(int $is_login = 0,int $remember = 0): Response
|
||||
{
|
||||
$data = Yii::$app->request->getRawBody();
|
||||
|
||||
@ -787,9 +834,19 @@ class UserController extends Controller
|
||||
return $this->asJson(['message' => $e->getMessage(), 'verified' => false]);
|
||||
}
|
||||
|
||||
|
||||
if ($is_login === 1) {
|
||||
$user = User::findOne(['id' => $publicKeyCredentialSourceRepository1->user_id]);
|
||||
$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, $remember===1 ? 3600 * 24 * 30 : 0);
|
||||
}
|
||||
// Optional, but highly recommended, you can save the credential source as it may be modified
|
||||
// during the verification process (counter may be higher).
|
||||
$publicKeyCredentialSourceRepository1->saveCredential($publicKeyCredentialSource, '',false);
|
||||
$publicKeyCredentialSourceRepository1->saveCredential($publicKeyCredentialSource, '', false);
|
||||
return $this->asJson(['verified' => true]);
|
||||
}
|
||||
|
||||
|
@ -69,10 +69,10 @@ class User extends ActiveRecord implements IdentityInterface
|
||||
['input_vault_secret', 'string', 'min' => 6, 'max' => 24],
|
||||
[['encryption_key', 'otp_secret', 'recovery_codes', 'vault_secret'], 'string', 'max' => 255],
|
||||
[['last_login_ip'], 'string', 'max' => 45],
|
||||
[['username', 'password'], 'required', 'on' => 'login'],
|
||||
[['username'], 'required', 'on' => 'login'],
|
||||
[['username', 'password', 'email', 'password2'], 'required', 'on' => 'register'],
|
||||
['username', 'string', 'min' => 3, 'max' => 12],
|
||||
['password', 'string', 'min' => 6, 'max' => 24, 'on' => 'register'],
|
||||
['password', 'string', 'min' => 6, 'max' => 24],
|
||||
['password2', 'compare', 'compareAttribute' => 'password', 'on' => 'register'],
|
||||
['email', 'email', 'on' => 'register'],
|
||||
['username', 'unique', 'on' => 'register'],
|
||||
|
54
views/user/_login1.php
Normal file
54
views/user/_login1.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// username+password verify
|
||||
use yii\bootstrap5\Html;
|
||||
use yii\bootstrap5\ActiveForm;
|
||||
use yii\helpers\Url;
|
||||
use yii\web\JqueryAsset;
|
||||
use yii\web\View;
|
||||
use yii\widgets\Pjax;
|
||||
|
||||
/** @var yii\web\View $this */
|
||||
/** @var app\models\User $model */
|
||||
/** @var ActiveForm $form */
|
||||
$this->title = '用户登录';
|
||||
$this->params['breadcrumbs'][] = $this->title;
|
||||
$verifyProvider = Yii::$app->params['verifyProvider'];
|
||||
if ($verifyProvider === 'reCAPTCHA') {
|
||||
$this->registerJsFile('https://www.recaptcha.net/recaptcha/api.js?hl=zh-CN', ['async' => true, 'defer' => true]);
|
||||
} elseif ($verifyProvider === 'hCaptcha') {
|
||||
$this->registerJsFile('https://js.hcaptcha.com/1/api.js?hl=zh-CN', ['async' => true, 'defer' => true]);
|
||||
} elseif ($verifyProvider === 'Turnstile') {
|
||||
$this->registerJsFile('https://challenges.cloudflare.com/turnstile/v0/api.js', ['async' => true, 'defer' => true]);
|
||||
} ?>
|
||||
<div class="user-login">
|
||||
<h1><?= Html::encode($this->title) ?></h1>
|
||||
|
||||
<p>我们已经找到了你的账户,请在下方输入你的密码以继续:</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
<?php $form = ActiveForm::begin(['action' => [Url::to('user/login')]]); ?>
|
||||
|
||||
<?= $form->field($model, 'username')->label('用户名')->textInput(['autofocus' => true, 'readonly' => true]) ?>
|
||||
<?= $form->field($model, 'password')->passwordInput()->label('密码') ?>
|
||||
<?= $form->field($model, 'rememberMe')->checkbox()->label('记住本次登录') ?>
|
||||
<div class="form-group">
|
||||
<?php if ($verifyProvider === 'reCAPTCHA'): ?>
|
||||
<div class="g-recaptcha" data-sitekey="<?= Yii::$app->params['reCAPTCHA']['siteKey'] ?>"></div>
|
||||
<?php elseif ($verifyProvider === 'hCaptcha'): ?>
|
||||
<div class="h-captcha" data-sitekey="<?= Yii::$app->params['hCaptcha']['siteKey'] ?>"></div>
|
||||
<?php elseif ($verifyProvider === 'Turnstile'): ?>
|
||||
<div class="cf-turnstile" data-sitekey="<?= Yii::$app->params['Turnstile']['siteKey'] ?>"
|
||||
data-language="zh-cn"></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<?= Html::submitButton('登录', ['class' => 'btn btn-primary']) ?>
|
||||
</div>
|
||||
<?php ActiveForm::end(); ?>
|
||||
<div class="form-group">
|
||||
<?= Html::a('回到上个页面', [Url::to('user/login')]) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- user-login -->
|
45
views/user/_login2.php
Normal file
45
views/user/_login2.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// username+fido verify
|
||||
use app\assets\SimpleWebAuthnBrowser;
|
||||
use yii\bootstrap5\Html;
|
||||
use yii\bootstrap5\ActiveForm;
|
||||
use yii\helpers\Url;
|
||||
use yii\web\JqueryAsset;
|
||||
use yii\web\View;
|
||||
|
||||
/** @var yii\web\View $this */
|
||||
/** @var app\models\User $model */
|
||||
/** @var ActiveForm $form */
|
||||
JqueryAsset::register($this);
|
||||
SimpleWebAuthnBrowser::register($this);
|
||||
$this->title = '用户登录';
|
||||
$this->params['breadcrumbs'][] = $this->title;
|
||||
?>
|
||||
<div class="user-login">
|
||||
<h1><?= Html::encode($this->title) ?></h1>
|
||||
|
||||
<p>我们已经找到了你的账户,请在下方验证Passkey以继续:</p>
|
||||
<div class="alert alert-success" role="alert" hidden>
|
||||
<span id="webauthn_success"></span>
|
||||
</div>
|
||||
<div class="alert alert-danger" role="alert" hidden>
|
||||
<span id="webauthn_error"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
<?php $form = ActiveForm::begin(['action' => '#']); ?>
|
||||
<?= $form->field($model, 'username')->label('用户名')->textInput(['autofocus' => true, 'readonly' => true, 'id' => 'username']) ?>
|
||||
<?= $form->field($model, 'rememberMe')->checkbox(['id' => 'rememberMe'])->label('记住本次登录') ?>
|
||||
<div class="form-group">
|
||||
<?= Html::button('登录', ['class' => 'btn btn-primary', 'id' => 'webauthn_verify']) ?>
|
||||
</div>
|
||||
<?php ActiveForm::end(); ?>
|
||||
<div class="form-group">
|
||||
<?= Html::a('不想使用Passkey? 回到传统登录方式', Url::to(['user/login', 'username' => $model->username])) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- user-login -->
|
||||
<?php
|
||||
$this->registerJsFile('@web/js/login-core.js', ['depends' => [JqueryAsset::class], 'position' => View::POS_END]);
|
||||
?>
|
@ -2,52 +2,33 @@
|
||||
|
||||
use yii\bootstrap5\Html;
|
||||
use yii\bootstrap5\ActiveForm;
|
||||
use yii\web\JqueryAsset;
|
||||
use yii\web\View;
|
||||
use yii\widgets\Pjax;
|
||||
|
||||
/** @var yii\web\View $this */
|
||||
/** @var app\models\User $model */
|
||||
/** @var ActiveForm $form */
|
||||
|
||||
$this->title = '用户登录';
|
||||
$this->params['breadcrumbs'][] = $this->title;
|
||||
$verifyProvider = Yii::$app->params['verifyProvider'];
|
||||
if ($verifyProvider === 'reCAPTCHA') {
|
||||
$this->registerJsFile('https://www.recaptcha.net/recaptcha/api.js?hl=zh-CN', ['async' => true, 'defer' => true]);
|
||||
} elseif ($verifyProvider === 'hCaptcha') {
|
||||
$this->registerJsFile('https://js.hcaptcha.com/1/api.js?hl=zh-CN', ['async' => true, 'defer' => true]);
|
||||
} elseif ($verifyProvider === 'Turnstile') {
|
||||
$this->registerJsFile('https://challenges.cloudflare.com/turnstile/v0/api.js', ['async' => true, 'defer' => true]);
|
||||
} ?>
|
||||
?>
|
||||
<div class="user-login">
|
||||
<h1><?= Html::encode($this->title) ?></h1>
|
||||
|
||||
<p>请在下方输入你的用户凭证:</p>
|
||||
<p>请在下方输入你的用户名:</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
<?php $form = ActiveForm::begin(); ?>
|
||||
|
||||
<?= $form->field($model, 'username')->label('用户名')->textInput(['autofocus' => true]) ?>
|
||||
<?= $form->field($model, 'password')->passwordInput()->label('密码') ?>
|
||||
<?= $form->field($model, 'rememberMe')->checkbox()->label('记住本次登录') ?>
|
||||
<div class="form-group">
|
||||
<?php if ($verifyProvider === 'reCAPTCHA'): ?>
|
||||
<div class="g-recaptcha" data-sitekey="<?= Yii::$app->params['reCAPTCHA']['siteKey'] ?>"></div>
|
||||
<?php elseif ($verifyProvider === 'hCaptcha'): ?>
|
||||
<div class="h-captcha" data-sitekey="<?= Yii::$app->params['hCaptcha']['siteKey'] ?>"></div>
|
||||
<?php elseif ($verifyProvider === 'Turnstile'): ?>
|
||||
<div class="cf-turnstile" data-sitekey="<?= Yii::$app->params['Turnstile']['siteKey'] ?>" data-language="zh-cn"></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<?= Html::submitButton('登录', ['class' => 'btn btn-primary']) ?>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<?= Html::a('还没有账户? 点击注册', ['user/register']) ?>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<?= Html::a('忘记密码?', ['user/forget']) ?>
|
||||
<?= Html::submitButton('下一步', ['class' => 'btn btn-primary']) ?>
|
||||
</div>
|
||||
<?php ActiveForm::end(); ?>
|
||||
<div class="form-group">
|
||||
<?= Html::a('还没有账户? 去注册', ['user/register']) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- user-login -->
|
||||
</div><!-- user-login -->
|
47
web/js/login-core.js
Normal file
47
web/js/login-core.js
Normal file
@ -0,0 +1,47 @@
|
||||
const elemSuccess = document.getElementById('webauthn_success');
|
||||
const elemError = document.getElementById('webauthn_error');
|
||||
const { startAuthentication } = SimpleWebAuthnBrowser;
|
||||
const elemBegin_v = document.getElementById('webauthn_verify');
|
||||
const username_input = document.getElementById('username');
|
||||
const remember = document.getElementById('rememberMe');
|
||||
|
||||
elemBegin_v.addEventListener('click', async () => {
|
||||
elemSuccess.innerHTML = '';
|
||||
elemError.innerHTML = '';
|
||||
elemSuccess.parentElement.hidden = true;
|
||||
elemError.parentElement.hidden = true;
|
||||
const username = encodeURIComponent(username_input.value);
|
||||
const resp = await fetch(`index.php?r=user%2Frequest-assertion-options&username=${username}`);
|
||||
let asseResp;
|
||||
try {
|
||||
asseResp = await startAuthentication(await resp.json());
|
||||
} catch (error) {
|
||||
elemError.innerText = error;
|
||||
elemError.parentElement.hidden = false;
|
||||
throw error;
|
||||
}
|
||||
const isChecked = remember.checked? 1 : 0;
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
const verificationResp = await fetch(`index.php?r=user%2Fverify-assertion&is_login=1&remember=${isChecked}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify(asseResp),
|
||||
});
|
||||
const verificationJSON = await verificationResp.json();
|
||||
if (verificationJSON && verificationJSON.verified) {
|
||||
elemSuccess.innerHTML = '登录成功!1s后跳转到首页';
|
||||
elemSuccess.parentElement.hidden = false;
|
||||
setTimeout(() => {
|
||||
window.location.href = 'index.php';
|
||||
}, 1000);
|
||||
|
||||
} else {
|
||||
elemError.innerHTML = `Oh no, something went wrong! Response: <pre>${JSON.stringify(
|
||||
verificationJSON,
|
||||
)}</pre>`;
|
||||
elemError.parentElement.hidden = false;
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user