增加 OAuth 登录

This commit is contained in:
iVampireSP.com 2023-03-11 14:21:21 +08:00
parent f12758b3a2
commit 162b7a6b4e
No known key found for this signature in database
GPG Key ID: 2F7B001CA27A8132
6 changed files with 117 additions and 169 deletions

View File

@ -107,3 +107,9 @@ ROADRUNNER_VERSION=
# 实人认证的配置
SUPPORT_REAL_NAME_APP_CODE=
# OAuth
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
OAUTH_REDIRECT=${APP_URL}/auth/callback
OAUTH_DOMAIN=http://oauth.test

View File

@ -5,15 +5,19 @@
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Notifications\User\UserNotification;
use function back;
use function config;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\View\View;
use function back;
use function config;
use function redirect;
use function session;
use function view;
@ -33,13 +37,13 @@ public function index(Request $request): View|RedirectResponse
$dashboardHost = parse_url(config('settings.dashboard.base_url'), PHP_URL_HOST);
if ($callbackHost === $dashboardHost) {
if (! $request->user('web')->isRealNamed()) {
if (!$request->user('web')->isRealNamed()) {
return redirect()->route('real_name.create')->with('status', '重定向已被打断,需要先实人认证。');
}
$token = $request->user()->createToken('Dashboard')->plainTextToken;
return redirect($callback.'?token='.$token);
return redirect($callback . '?token=' . $token);
}
session(['referer.domain' => parse_url($request->header('referer'), PHP_URL_HOST)]);
@ -124,7 +128,7 @@ public function exitSudo(): RedirectResponse
public function showAuthRequest($token): View|RedirectResponse
{
$data = Cache::get('auth_request:'.$token);
$data = Cache::get('auth_request:' . $token);
if (empty($data)) {
return redirect()->route('index')->with('error', '登录请求的 Token 不存在或已过期。');
@ -148,7 +152,7 @@ public function storeAuthRequest(Request $request): RedirectResponse|View
'token' => 'required|string|max:128',
]);
$data = Cache::get('auth_request:'.$request->input('token'));
$data = Cache::get('auth_request:' . $request->input('token'));
if (empty($data)) {
return back()->with('error', '登录请求的 Token 不存在或已过期。');
@ -172,10 +176,10 @@ public function storeAuthRequest(Request $request): RedirectResponse|View
$data['token'] = $user->createToken($data['meta']['description'] ?? Carbon::now()->toDateString(), $abilities)->plainTextToken;
}
Cache::put('auth_request:'.$request->input('token'), $data, 60);
Cache::put('auth_request:' . $request->input('token'), $data, 60);
if (isset($data['meta']['return_url']) && $data['meta']['return_url']) {
return redirect()->to($data['meta']['return_url'].'?auth_request='.$request->input('token'));
return redirect()->to($data['meta']['return_url'] . '?auth_request=' . $request->input('token'));
}
return redirect()->route('index')->with('success', '登录请求已确认。');
@ -183,8 +187,8 @@ public function storeAuthRequest(Request $request): RedirectResponse|View
public function fastLogin(string $token): RedirectResponse
{
$cache_key = 'session_login:'.$token;
$user_id = Cache::get('session_login:'.$token);
$cache_key = 'session_login:' . $token;
$user_id = Cache::get('session_login:' . $token);
if (empty($user_id)) {
return redirect()->route('index')->with('error', '登录请求的 Token 不存在或已过期。');
@ -202,4 +206,81 @@ public function fastLogin(string $token): RedirectResponse
return redirect()->route('index');
}
public function redirect(Request $request)
{
$request->session()->put('state', $state = Str::random(40));
$query = http_build_query([
'client_id' => config('oauth.client_id'),
'redirect_uri' => config('oauth.callback_uri'),
'response_type' => 'code',
'scope' => config('oauth.scope'),
'state' => $state,
]);
return redirect()->to(config('oauth.oauth_auth_url') . '?' . $query);
}
public function callback(Request $request)
{
try {
$authorize = Http::asForm()->throw()->post(config('oauth.oauth_token_url'), [
'grant_type' => 'authorization_code',
'client_id' => config('oauth.client_id'),
'client_secret' => config('oauth.client_secret'),
'redirect_uri' => config('oauth.callback_uri'),
'code' => $request->input('code'),
])->getBody();
} catch (Exception $e) {
return redirect()->route('index')->with('error', '无法获取授权。' . $e->getMessage());
}
$authorize = json_decode($authorize);
$http = Http::asForm()->withHeaders([
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $authorize->access_token
])->throw();
$oauth_user = $http->get(config('oauth.oauth_user_url'))->object();
$real_name = $http->get(config('oauth.oauth_real_name_url'))->object();
$user_sql = User::where('email', $oauth_user->email);
$user = $user_sql->first();
if (is_null($user)) {
$name = $oauth_user->name;
$email = $oauth_user->email;
$email_verified_at = $oauth_user->email_verified_at;
$user = new User();
$user->name = $name;
$user->email = $email;
$user->email_verified_at = $email_verified_at;
$user->real_name_verified_at = $real_name->real_name_verified_at;
$user->real_name = $real_name->real_name;
$user->id_card = $real_name->id_card;
$user->password = Hash::make(Str::random(16));
$user->save();
$request->session()->put('auth.password_confirmed_at', time());
} else {
if ($user->name != $oauth_user->name) {
User::where('email', $oauth_user->email)->update([
'name' => $oauth_user->name
]);
}
}
Auth::guard('web')->loginUsingId($user->id, true);
return redirect()->route('index');
}
}

View File

@ -158,7 +158,7 @@ public function selectPublic(): self|Builder|CachedBuilder
// 仅需选择公开的
return $this->select($this->publics);
}
public function getOnlyPublic($appened_excepts = [], $display = []): array
{
if ($display) {

12
config/oauth.php Normal file
View File

@ -0,0 +1,12 @@
<?php
return [
'callback_uri' => env('OAUTH_REDIRECT'),
'client_id' => env('OAUTH_CLIENT_ID'),
'client_secret' => env('OAUTH_CLIENT_SECRET'),
'oauth_auth_url' => env('OAUTH_DOMAIN') . '/oauth/authorize',
'oauth_token_url' => env('OAUTH_DOMAIN') . '/oauth/token',
'oauth_user_url' => env('OAUTH_DOMAIN') . '/api/user',
'oauth_real_name_url' => env('OAUTH_DOMAIN') . '/api/real-name',
'scope' => 'realname login user',
];

View File

@ -8,169 +8,15 @@
</span>
<h2 id="form-title">注册 登录</h2>
<h2 id="form-title">使用 LoliArt Account 登录。</h2>
<form id="main-form" method="POST" onsubmit="return canSubmit()">
@csrf
<a class="btn btn-primary" href="{{route('login')}}">使用 LoliArt Account 登录</a>
<div class="form-group">
<input type="email" name="email" id="email" class="form-control mb-3 text-center" placeholder="邮箱"
aria-label="邮箱" required autofocus>
</div>
<div id="suffix-form"></div>
</form>
<br/>
<a class="link" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
</div>
</div>
<div class="d-none">
<div id="password-input">
<div class="form-group mt-2">
<input type="password" id="password" name="password"
class="form-control rounded-right text-center @error('password') is-invalid @enderror" required
placeholder="密码" aria-label="密码">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group mt-2" id="password-confirm-input">
<label for="password-confirm">确认密码</label>
<input type="password" id="password-confirm" name="password_confirmation"
class="form-control rounded-right" required autocomplete="new-password"
placeholder="再次输入您的密码">
</div>
<div id="remember-form">
<input class="form-check-input" type="hidden" id="remember" name="remember" value="1">
</div>
<small id="tip" class="d-block"></small>
<div class="mt-1" id="tos">如果您继续,则代表您已经阅读并同意 <a
href="https://www.laecloud.com/tos/"
target="_blank"
class="text-decoration-underline">服务条款</a>
</div>
<button class="btn btn-primary btn-block mt-2" type="submit" id="login-btn">
继续
</button>
</div>
<script>
const login = "{{ route('login') }}"
const register = "{{ route('register') }}"
const mainIcon = document.getElementById('main-icon')
const email = document.getElementById('email');
const title = document.getElementById('form-title');
const formSuffix = document.getElementById('suffix-form')
const rememberForm = document.getElementById('remember-form')
const passwordInput = document.getElementById('password-input')
const passwordConfirmInput = document.getElementById('password-confirm-input')
const loginBtn = document.getElementById('login-btn')
const nameInput = document.getElementById('name')
const mainForm = document.getElementById('main-form')
const tos = document.getElementById('tos')
const tip = document.getElementById('tip')
@error('password')
title.innerText = "注册莱云"
formSuffix.appendChild(rememberForm)
@enderror
@error('email')
title.innerText = "密码错误"
email.value = "{{ old('email') }}"
formSuffix.appendChild(passwordInput)
formSuffix.appendChild(rememberForm)
formSuffix.appendChild(tos)
formSuffix.appendChild(loginBtn)
loginBtn.innerText = '登录'
@enderror
let canSubmit = function () {
return (email.value !== '' && passwordInput.value !== '')
}
const validateUrl = "{{ route('login.exists-if-user') }}"
email.onchange = function (ele) {
const target = ele.target
if (email.value === '') {
title.innerText = "输入邮箱"
formSuffix.innerHTML = ''
mainIcon.classList.remove(...mainIcon.classList)
mainIcon.classList.add('bi', 'bi-person-circle')
return
}
formSuffix.innerHTML = ''
formSuffix.appendChild(passwordInput)
axios.post(validateUrl, {
email: target.value
})
.then(function (res) {
mainForm.action = login
mainIcon.classList.remove(...mainIcon.classList)
mainIcon.classList.add('bi', 'bi-person-check')
title.innerText = "欢迎, " + res.data.name
formSuffix.appendChild(passwordInput)
formSuffix.appendChild(rememberForm)
formSuffix.appendChild(tos)
formSuffix.appendChild(loginBtn)
loginBtn.innerText = '登录'
})
.catch(function () {
mainForm.action = register
title.innerText = "注册莱云"
mainIcon.classList.remove(...mainIcon.classList)
mainIcon.classList.add('bi', 'bi-person-plus')
formSuffix.appendChild(passwordInput)
formSuffix.appendChild(tos)
formSuffix.appendChild(tip)
formSuffix.appendChild(loginBtn)
tip.innerText = '当您注册后,我们将为您分配随机用户名。'
loginBtn.innerText = '注册'
});
}
</script>
@endsection

View File

@ -19,8 +19,8 @@
Route::get('/', [AuthController::class, 'index'])->middleware('banned')->name('index');
Route::prefix('auth')->group(function () {
Route::get('login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('login', [LoginController::class, 'login']);
// Route::get('login', [LoginController::class, 'showLoginForm'])->name('login');
// Route::post('login', [LoginController::class, 'login']);
Route::post('logout', [LoginController::class, 'logout'])->name('logout');
Route::post('exists', [LoginController::class, 'userIfExists'])->name('login.exists-if-user');
@ -40,6 +40,9 @@
Route::post('email/resend', [VerificationController::class, 'resend'])->name('verification.resend');
Route::get('token/{token}', [AuthController::class, 'fastLogin'])->name('auth.fast-login');
Route::get('redirect', [AuthController::class, 'redirect'])->name('login');
Route::get('callback', [AuthController::class, 'callback'])->name('callback');
});
Route::middleware(['auth:web', 'banned', 'verified'])->group(