1.增加谷歌验证器
This commit is contained in:
122
app/common/library/AdminTotp.php
Normal file
122
app/common/library/AdminTotp.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use ba\Random;
|
||||
use app\common\facade\Token;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider;
|
||||
|
||||
/**
|
||||
* 管理员谷歌验证器(TOTP)
|
||||
*/
|
||||
class AdminTotp
|
||||
{
|
||||
public const TOKEN_TYPE_BIND = 'admin-totp-bind';
|
||||
public const TOKEN_TYPE_VERIFY = 'admin-totp-verify';
|
||||
public const TOKEN_EXPIRE = 300;
|
||||
|
||||
private static ?TwoFactorAuth $tfa = null;
|
||||
|
||||
private static function tfa(): TwoFactorAuth
|
||||
{
|
||||
if (self::$tfa === null) {
|
||||
$issuer = (string) get_sys_config('site_name');
|
||||
if ($issuer === '') {
|
||||
$issuer = 'BuildAdmin';
|
||||
}
|
||||
self::$tfa = new TwoFactorAuth(new BaconQrCodeProvider(), $issuer);
|
||||
}
|
||||
return self::$tfa;
|
||||
}
|
||||
|
||||
public static function isBound(?string $encryptedSecret): bool
|
||||
{
|
||||
return is_string($encryptedSecret) && $encryptedSecret !== '';
|
||||
}
|
||||
|
||||
public static function generateSecret(): string
|
||||
{
|
||||
return self::tfa()->createSecret();
|
||||
}
|
||||
|
||||
public static function getQrDataUri(string $label, string $secret): string
|
||||
{
|
||||
return self::tfa()->getQRCodeImageAsDataUri($label, $secret);
|
||||
}
|
||||
|
||||
public static function verifyCode(string $plainSecret, string $code): bool
|
||||
{
|
||||
$code = trim($code);
|
||||
if (!preg_match('/^\d{6}$/', $code)) {
|
||||
return false;
|
||||
}
|
||||
return self::tfa()->verifyCode($plainSecret, $code);
|
||||
}
|
||||
|
||||
public static function encryptSecret(string $plainSecret): string
|
||||
{
|
||||
$key = substr(hash('sha256', (string) config('buildadmin.token.key')), 0, 32);
|
||||
$iv = random_bytes(16);
|
||||
$encrypted = openssl_encrypt($plainSecret, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||
if ($encrypted === false) {
|
||||
return '';
|
||||
}
|
||||
return base64_encode($iv . $encrypted);
|
||||
}
|
||||
|
||||
public static function decryptSecret(string $encryptedSecret): string
|
||||
{
|
||||
if ($encryptedSecret === '') {
|
||||
return '';
|
||||
}
|
||||
$raw = base64_decode($encryptedSecret, true);
|
||||
if ($raw === false || strlen($raw) < 17) {
|
||||
return '';
|
||||
}
|
||||
$iv = substr($raw, 0, 16);
|
||||
$encrypted = substr($raw, 16);
|
||||
$key = substr(hash('sha256', (string) config('buildadmin.token.key')), 0, 32);
|
||||
$plain = openssl_decrypt($encrypted, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||
return $plain === false ? '' : $plain;
|
||||
}
|
||||
|
||||
public static function verifyStoredCode(string $encryptedSecret, string $code): bool
|
||||
{
|
||||
$plain = self::decryptSecret($encryptedSecret);
|
||||
if ($plain === '') {
|
||||
return false;
|
||||
}
|
||||
return self::verifyCode($plain, $code);
|
||||
}
|
||||
|
||||
public static function createPendingToken(int $adminId, string $type): string
|
||||
{
|
||||
$token = Random::uuid();
|
||||
Token::set($token, $type, $adminId, self::TOKEN_EXPIRE);
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function resolvePendingToken(string $token, string $type): int
|
||||
{
|
||||
$token = trim($token);
|
||||
if ($token === '') {
|
||||
return 0;
|
||||
}
|
||||
$data = Token::get($token);
|
||||
if (!$data || ($data['type'] ?? '') !== $type) {
|
||||
return 0;
|
||||
}
|
||||
Token::tokenExpirationCheck($data);
|
||||
return (int) ($data['user_id'] ?? 0);
|
||||
}
|
||||
|
||||
public static function deletePendingToken(string $token): void
|
||||
{
|
||||
if ($token !== '') {
|
||||
Token::delete($token);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user