兩種不同的元素,合併在一起,來確認使用者的身分。
有兩種密碼演算法
HOTP(HMAC-based One-time Password),RFC4226
金鑰和隨機資訊雜湊後,取N個字作為密碼
TOTP(Time-Based One-Time Password),RFC6238
透過時間來產生一次性密碼
邏輯如下:
呼叫API時先進到Verify2FA這個Middleware
檢查是否有使用帳號密碼登入過,沒有則讓他呼叫API
登入後如果2FA金鑰為空,回傳錯誤訊息,讓Front-End轉跳到顯示QRCode頁面
進入頁面呼叫get2FAQRCodeUrl,Front-End將URL轉成QRCode Image,接著使用者掃完後下一步
開始驗證2FA,驗證通過在Session儲存twoFAChecked為true
登入後如果還沒驗證過2FA,回傳錯誤訊息,讓Front-End轉跳到顯示輸入2FA驗證頁面
登出後將Session清空
首先安裝google2fa-laravel套件
composer require pragmarx/google2fa-laravel
Tables:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id()->comment('使用者ID');
$table->string('email')->nullable()->comment('電子郵件');
$table->string('password')->comment('密碼');
$table->integer('email_verified_at', unsigned: true)->nullable()->comment('郵件驗證時間');
$table->text('otp_secret')->nullable()->comment('2FA');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};
產生config
php artisan vendor:publish --provider="PragmaRX\Google2FALaravel\ServiceProvider"
config/google2fa.php
<?php
return [
/*
* Enable / disable Google2FA.
*/
'enabled' => env('OTP_ENABLED', true),
/*
* Lifetime in minutes.
*
* In case you need your users to be asked for a new one time passwords from time to time.
*/
'lifetime' => env('OTP_LIFETIME', 0), // 0 = eternal
/*
* Renew lifetime at every new request.
*/
'keep_alive' => env('OTP_KEEP_ALIVE', true),
/*
* Auth container binding.
*/
'auth' => 'auth',
/*
* Guard.
*/
'guard' => '',
/*
* 2FA verified session var.
*/
'session_var' => 'google2fa',
/*
* One Time Password request input name.
*/
'otp_input' => 'one_time_password',
/*
* One Time Password Window.
*/
'window' => 1,
/*
* Forbid user to reuse One Time Passwords.
*/
'forbid_old_passwords' => false,
/*
* User's table column for google2fa secret.
*/
'otp_secret_column' => 'otp_secret',
/*
* One Time Password View.
*/
'view' => 'google2fa.index',
/*
* One Time Password error message.
*/
'error_messages' => [
'wrong_otp' => "The 'One Time Password' typed was wrong.",
'cannot_be_empty' => 'One Time Password cannot be empty.',
'unknown' => 'An unknown error has occurred. Please try again.',
],
/*
* Throw exceptions or just fire events?
*/
'throw_exceptions' => env('OTP_THROW_EXCEPTION', true),
/*
* Which image backend to use for generating QR codes?
*
* Supports imagemagick, svg and eps
*/
'qrcode_image_backend' => \PragmaRX\Google2FALaravel\Support\Constants::QRCODE_IMAGE_BACKEND_SVG,
];
app/Http/Kernel.php
<?php
protected $routeMiddleware = [
...
'2FA' => Verify2FA::class,
];
Middleware:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class Verify2FA
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
$lang = trim($request->header('lang'));
//尚未登入
if (!auth()->check()) {
return $next($request);
}
//如果2FA 金鑰為空則直接回傳
if (is_null(auth()->user()->otp_secret)) {
return response()->json([
'msg' => 'abc',
'err' => 1,
]);
}
if (session('twoFAChecked', false)) {
return response()->json([
'msg' => 'abc',
'err' => 2,
]);
}
return $next($request);
}
}
<?php
namespace App\Services\v1\user;
use App\Repositories\v1\UserRepository;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException;
use PragmaRX\Google2FA\Exceptions\InvalidCharactersException;
use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException;
use PragmaRX\Google2FA\Google2FA;
class UserService
{
/**
* Construct
* @param Google2FA $google2fa
* @param UserRepository $userRepo
*/
public function __construct(protected Google2FA $google2fa, protected UserRepository $userRepo)
{
}
/**
* Login
* @param Request $request
* @return JsonResponse
*/
public function login(Request $request): JsonResponse
{
$lang = trim($request->header('lang'));
$username = trim($request->username);
$password = trim($request->password);
$user = $this->userRepo->find(['email' => $username]);
//取得新的Token
$token = auth()->attempt(['email' => $username, 'password' => $password]);
$info = [
//是否開啟OTP
'enableOTP' => $user->enable_otp,
'token' => $token,
];
return response()->json([
'data' => $info
]);
}
/**
* Admin 登出
* @param Request $request
* @return JsonResponse
*/
public function logout(Request $request): JsonResponse
{
$lang = trim($request->header('lang'));
auth()->logout();
//清除使用者的session
$request->session()->flush();
return response()->json([
'msg' => 'test',
'err' => 3,
]);
}
/**
* 取得2FA QRCode URL
* @param Request $request
* @return JsonResponse
* @throws IncompatibleWithGoogleAuthenticatorException
* @throws InvalidCharactersException
* @throws SecretKeyTooShortException
*/
public function get2FAQRCodeUrl(Request $request): JsonResponse
{
$lang = trim($request->header('lang'));
//取得當前使用者
$currentUser = auth()->user();
//email
$email = $currentUser->email;
//secret key
$secretKey = $currentUser->otp_secret;
//如果加密金鑰不存在則產生並更新到user
if (empty($secretKey)) {
$secretKey = $this->google2fa->generateSecretKey();
$this->userRepo->modifyData($currentUser->id, ['otp_secret' => $secretKey]);
}
$url = $this->google2fa->getQRCodeURL('Company Name', $email, $secretKey);
return response()->json([
'msg' => 'test',
'data' => [
'url'=>$url
],
]);
}
/**
* 兩部驗證
* @param Request $request
* @return JsonResponse
* @throws IncompatibleWithGoogleAuthenticatorException
* @throws InvalidCharactersException
* @throws SecretKeyTooShortException
*/
public function twoFAVerify(Request $request)
{
//OTP密碼
$otp = trim($request->otp);
$currentUser = auth()->user();
//取得當前的使用者key
$secretKey = $currentUser->otp_secret;
$verify = $this->google2fa->verifyKey($secretKey, $otp);
//驗證失敗
if (!$verify) {
return response()->json([
'msg' => 'test',
'err' => 4,
]);
}
//寫入twoFAChecked
session('twoFAChecked', true);
//第一次驗證
if (!$currentUser->enable_otp) {
//更新已開啟驗證
$this->userRepo->modifyData($currentUser->id, ['enable_otp' => 1]);
}
return response()->json();
}
}
Routes:
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
/*
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
*/
/**未登入*/
Route::prefix('v1')->middleware('api')->group(function () {
Route::prefix('user')->group(function () {
//Admin login
Route::post('/login', [\App\Http\Controllers\v1\user\UserController::class, 'login']);
/** Admin已登入情況下 */
Route::middleware(['auth:user'])->group(function () {
/**
* 2FA
*/
Route::prefix('2FA')->group(function () {
//取得2FA QRCode URL
Route::get('/get2FAQRCodeUrl', [\App\Http\Controllers\v1\user\UserController::class, 'get2FAQRCodeUrl']);
//2FA驗證
Route::post('/twoFAVerify', [\App\Http\Controllers\v1\user\UserController::class, 'twoFAVerify']);
});
/**
* 2FA已登入情況下
*/
Route::middleware('2FA')->group(function () {
//登出
Route::post('/logout', [\App\Http\Controllers\v1\user\UserController::class, 'logout']);
});
});
});
});