Selaa lähdekoodia

加上GA验证

Tree 1 viikko sitten
vanhempi
sitoutus
923309ac2f

+ 1 - 1
app/AdminUser.php

@@ -13,7 +13,7 @@ use Illuminate\Support\Facades\Hash;
 class AdminUser extends Model
 {
     protected $connection = 'write';
-    protected $fillable = ['avatar', 'nickname', 'account', 'password', 'lottery_amount', 'recharge_amount', 'type','channel','locale'];
+    protected $fillable = ['avatar', 'nickname', 'account', 'password', 'lottery_amount', 'recharge_amount', 'type','channel','locale', 'ga_secret', 'ga_enabled'];
     protected $hidden = ['password'];
 
     public function roles()

+ 60 - 1
app/Http/Controllers/Admin/AdministratorController.php

@@ -14,6 +14,7 @@ use App\AdminRole;
 use App\AdminUser;
 use App\Http\Controllers\Controller;
 use App\Http\helper\Helper;
+use App\Services\GoogleAuthenticatorService;
 use App\Utility\Rbac;
 use Illuminate\Http\Request;
 use Illuminate\Support\Collection;
@@ -392,12 +393,14 @@ class AdministratorController extends Controller
         $channels = DB::table('QPPlatformDB.dbo.ChannelPackageName')
                       ->pluck('Channel', 'Channel');
 
+        $ga = $this->buildGaPayload($admin);
 
         return view('admin.administrator_update', [
             'admin' => $admin,
             'roles' => $roles,
             'channels'=>$channels,
             's_role_id_arr' => $selectRoleIdArr,
+            'ga' => $ga,
 
         ]);
     }
@@ -411,7 +414,17 @@ class AdministratorController extends Controller
             return $this->json(500, '该账号已存在');
         }
         $post['channel'] = json_encode($post['channel']);
-        $post = array_filter($post);
+        $post['ga_enabled'] = empty($post['ga_enabled']) ? 0 : 1;
+
+        if ($post['ga_enabled'] == 1 && empty($post['ga_secret'])) {
+            return $this->json(500, '请先生成GA密钥');
+        }
+        $post = array_filter($post, function ($value, $key) {
+            if ($key === 'ga_enabled') {
+                return true;
+            }
+            return !($value === null || $value === '');
+        }, ARRAY_FILTER_USE_BOTH);
 
         $admin->fill($post)->save();
 
@@ -426,6 +439,19 @@ class AdministratorController extends Controller
 
     }
 
+    public function resetGaSecret($id)
+    {
+        $admin = AdminUser::findOrFail($id);
+        $gaService = new GoogleAuthenticatorService();
+        $secret = $gaService->generateSecret(32);
+
+        $admin->ga_secret = $secret;
+        $admin->ga_enabled = 0;
+        $admin->save();
+
+        return $this->json(200, 'GA密钥已重置,请扫码后再启用', $this->buildGaPayload($admin));
+    }
+
     /**
      * @return mixed
      * 删除管理员
@@ -547,6 +573,18 @@ class AdministratorController extends Controller
         if ($admin->status == -1) {
             return $this->json(500, trans('cs.login.block'));
         }
+        if ((int) $admin->ga_enabled !== 1 || empty($admin->ga_secret)) {
+            return $this->json(500, trans('cs.login.ga_required'));
+        }
+
+        $gaCode = isset($post['ga_code']) ? trim((string) $post['ga_code']) : '';
+        if ($gaCode === '') {
+            return $this->json(500, trans('cs.login.notice_ga_code'));
+        }
+        $gaService = new GoogleAuthenticatorService();
+        if (!$gaService->verifyCode($admin->ga_secret, $gaCode)) {
+            return $this->json(500, trans('cs.login.wrong_ga_code'));
+        }
 
         $roles = $admin->roles;
 
@@ -623,4 +661,25 @@ class AdministratorController extends Controller
         return redirect('/admin/login_op');
     }
 
+    protected function buildGaPayload(AdminUser $admin)
+    {
+        $secret = $admin->ga_secret ?: '';
+        $gaService = new GoogleAuthenticatorService();
+        $issuer = config('app.name', 'Admin');
+        $otpAuthUrl = '';
+        $qrCodeUrl = '';
+
+        if ($secret !== '') {
+            $otpAuthUrl = $gaService->getOtpAuthUrl($issuer, $admin->account, $secret);
+            $qrCodeUrl = $gaService->getQrCodeUrl($otpAuthUrl, 200);
+        }
+
+        return [
+            'secret' => $secret,
+            'otpauth_url' => $otpAuthUrl,
+            'qr_code_url' => $qrCodeUrl,
+            'enabled' => (int) $admin->ga_enabled,
+        ];
+    }
+
 }

+ 93 - 0
app/Services/GoogleAuthenticatorService.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace App\Services;
+
+class GoogleAuthenticatorService
+{
+    const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
+
+    public function generateSecret($length = 32)
+    {
+        $alphabet = self::BASE32_ALPHABET;
+        $max = strlen($alphabet) - 1;
+        $secret = '';
+        for ($i = 0; $i < $length; $i++) {
+            $secret .= $alphabet[random_int(0, $max)];
+        }
+        return $secret;
+    }
+
+    public function verifyCode($secret, $code, $window = 1, $period = 30, $digits = 6)
+    {
+        $code = trim((string) $code);
+        if ($code === '' || !preg_match('/^\d{6}$/', $code)) {
+            return false;
+        }
+
+        $timeSlice = (int) floor(time() / $period);
+        for ($i = -$window; $i <= $window; $i++) {
+            $calc = $this->getCode($secret, $timeSlice + $i, $digits);
+            if (hash_equals($calc, $code)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public function getOtpAuthUrl($issuer, $account, $secret)
+    {
+        $label = rawurlencode($issuer . ':' . $account);
+        $issuerParam = rawurlencode($issuer);
+        return "otpauth://totp/{$label}?secret={$secret}&issuer={$issuerParam}&algorithm=SHA1&digits=6&period=30";
+    }
+
+    public function getQrCodeUrl($otpAuthUrl, $size = 200)
+    {
+        $encoded = rawurlencode($otpAuthUrl);
+        return "https://api.qrserver.com/v1/create-qr-code/?size={$size}x{$size}&data={$encoded}";
+    }
+
+    protected function getCode($secret, $timeSlice, $digits)
+    {
+        $secretKey = $this->base32Decode($secret);
+        if ($secretKey === '') {
+            return '';
+        }
+
+        $time = pack('N*', 0) . pack('N*', $timeSlice);
+        $hash = hash_hmac('sha1', $time, $secretKey, true);
+        $offset = ord(substr($hash, -1)) & 0x0F;
+        $truncatedHash = substr($hash, $offset, 4);
+        $value = unpack('N', $truncatedHash)[1] & 0x7FFFFFFF;
+        $modulo = pow(10, $digits);
+        return str_pad((string) ($value % $modulo), $digits, '0', STR_PAD_LEFT);
+    }
+
+    protected function base32Decode($secret)
+    {
+        $secret = strtoupper($secret);
+        $secret = preg_replace('/[^A-Z2-7]/', '', $secret);
+        if ($secret === '') {
+            return '';
+        }
+
+        $alphabet = array_flip(str_split(self::BASE32_ALPHABET));
+        $binary = '';
+        $buffer = 0;
+        $bitsLeft = 0;
+
+        foreach (str_split($secret) as $char) {
+            if (!isset($alphabet[$char])) {
+                continue;
+            }
+            $buffer = ($buffer << 5) | $alphabet[$char];
+            $bitsLeft += 5;
+            while ($bitsLeft >= 8) {
+                $bitsLeft -= 8;
+                $binary .= chr(($buffer >> $bitsLeft) & 0xFF);
+            }
+        }
+
+        return $binary;
+    }
+}

+ 4 - 0
resources/lang/en_US/cs.php

@@ -29,6 +29,10 @@ return [
         'wrong_captcha' => 'Invalid or expired code. Refresh and try again.',
         'captcha_placeholder' => 'Verification code',
         'captcha_refresh' => 'Click to refresh',
+        'ga_code_placeholder' => 'Google code (6 digits)',
+        'notice_ga_code' => 'Please enter Google verification code!',
+        'wrong_ga_code' => 'Invalid Google verification code!',
+        'ga_required' => 'GA is not configured for this account. Contact admin first.',
         'cannotfinduser' => '账号不存在!',
         'wrongpass' => '密码输入不正确!',
         'block' => '账号已被禁用!',

+ 4 - 0
resources/lang/zh_CN/cs.php

@@ -30,6 +30,10 @@ return [
         'wrong_captcha' => '验证码错误或已过期,请刷新后重试!',
         'captcha_placeholder' => '验证码',
         'captcha_refresh' => '点击刷新',
+        'ga_code_placeholder' => 'Google验证码(6位)',
+        'notice_ga_code' => '请输入Google验证码!',
+        'wrong_ga_code' => 'Google验证码错误!',
+        'ga_required' => '账号未完成GA绑定,请联系管理员配置后再登录!',
         'cannotfinduser' => '账号不存在!',
         'wrongpass' => '密码输入不正确!',
         'block' => '账号已被禁用!',

+ 33 - 0
resources/views/admin/administrator_update.blade.php

@@ -102,6 +102,21 @@
                                     @endforeach
 
                                 </div>
+                                <div class="form-group">
+                                    <label>Google Authenticator</label>
+                                    <div class="input-group col-xs-12" style="margin-bottom: 10px;">
+                                        <input type="text" id="ga_secret" class="form-control" name="ga_secret" value="{{ $ga['secret'] }}" readonly>
+                                        <span class="input-group-append">
+                                            <button class="btn btn-gradient-info" type="button" onclick="resetGaSecret({{ $admin->id }})">重置GA密钥</button>
+                                        </span>
+                                    </div>
+                                    <div style="margin-bottom: 10px;">
+                                        <label><input type="checkbox" id="ga_enabled" name="ga_enabled" value="1" @if($ga['enabled'] == 1) checked @endif> 启用GA登录校验</label>
+                                    </div>
+                                    <div>
+                                        <img id="ga_qr_img" src="{{ $ga['qr_code_url'] }}" alt="ga-qr" style="max-width: 200px; border: 1px solid #eee; padding: 4px; @if(empty($ga['qr_code_url'])) display:none; @endif">
+                                    </div>
+                                </div>
                                 <button type="button" onclick="commit({{$admin->id}})"
                                         class="btn btn-sm btn-gradient-primary btn-icon-text">
                                     <i class="mdi mdi-file-check btn-icon-prepend"></i>
@@ -151,6 +166,8 @@
                     data.channel.push(opt.value)
                 }
             }
+            data.ga_enabled = $('#ga_enabled').is(':checked') ? 1 : 0
+            data.ga_secret = $('#ga_secret').val()
             myRequest("/admin/administrator/update/" + id, "post", data, function (res) {
                 if (res.code == '200') {
                     layer.msg(res.msg)
@@ -163,6 +180,22 @@
             });
         }
 
+        function resetGaSecret(id) {
+            myRequest("/admin/administrator/ga/reset/" + id, "post", {}, function (res) {
+                if (res.code == '200') {
+                    layer.msg(res.msg)
+                    var payload = res.data || {}
+                    $('#ga_secret').val(payload.secret || '')
+                    if (payload.qr_code_url) {
+                        $('#ga_qr_img').attr('src', payload.qr_code_url).show()
+                    }
+                    $('#ga_enabled').prop('checked', false)
+                } else {
+                    layer.msg(res.msg)
+                }
+            });
+        }
+
         function cancel() {
             parent.location.reload();
         }

+ 5 - 0
resources/views/admin/login.blade.php

@@ -29,6 +29,9 @@
                 <div class="form-group">
                   <input type="password" class="form-control form-control-lg" id="password" placeholder="{{trans('cs.login.notice_pass')}}">
                 </div>
+                <div class="form-group">
+                  <input type="text" class="form-control form-control-lg" id="ga_code" name="ga_code" autocomplete="off" maxlength="6" placeholder="谷歌验证码">
+                </div>
                 <div class="form-group">
                   <div class="input-group">
                     <input type="text" class="form-control form-control-lg" id="captcha" name="captcha" autocomplete="off" maxlength="6" placeholder="{{ trans('cs.login.captcha_placeholder') }}">
@@ -79,6 +82,7 @@
     function login(){
         var account = $("#account").val();
         var password = $("#password").val();
+        var gaCode = $("#ga_code").val();
         var captcha = $("#captcha").val();
         if(!account || !password){
             layer.msg('账号和密码不能为空', function(){});
@@ -91,6 +95,7 @@
         var data = {
             'account':account,
             'password':password,
+            'ga_code':gaCode,
             'captcha':captcha,
         };
         myRequest("/admin/login_op","post",data,function(res){

+ 1 - 0
routes/web.php

@@ -131,6 +131,7 @@ Route::group([
         $route->post('administrator/add', 'Admin\AdministratorController@administratorAdd');
         $route->get('administrator/update/{id}', 'Admin\AdministratorController@administratorUpdateView');
         $route->post('administrator/update/{id}', 'Admin\AdministratorController@administratorUpdate');
+        $route->post('administrator/ga/reset/{id}', 'Admin\AdministratorController@resetGaSecret');
         $route->post('administrator/del/{id}', 'Admin\AdministratorController@administratorDel');
         $route->post('administrator/block/{id}', 'Admin\AdministratorController@administratorBlock');
         //配置管理