добавил контроллер аутентификации, который срабатывает после ввода логина пароля (да, я знаю, что назвать надо было по другому)
This commit is contained in:
@@ -0,0 +1,246 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use LdapRecord\Models\ActiveDirectory\User as LdapUserInfo;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Facades\UserContext;
|
||||||
|
use App\Services\AuthorizationService;
|
||||||
|
use App\Services\ApiResponder;
|
||||||
|
use App\Dto\ApiResponseDto;
|
||||||
|
|
||||||
|
class LoginController extends Controller
|
||||||
|
{
|
||||||
|
#Гаврилов
|
||||||
|
//КОГДА ПЕРЕЙДЕМ НА ГРУППЫ, ПОМЕНЯТЬ НА АДМИНСКУЮ ГРУППУ? ИЛИ ОСТАВИТЬ РАССЫЛКУ?
|
||||||
|
//ПЕРЕНЕСИ В .ENV И ВНИЗУ ПО КОДУ ГДЕ ОБРАЩАЕШЬСЯ К ЭТОМУ СВОЙСТВУ КЛАССА ПЕРЕПИШИ НА ПОЛУЧЕНИЕ СВОЙСТВА ИЗ ПЕРЕМЕННОЙ ОКРУЖЕНИЯ
|
||||||
|
/**
|
||||||
|
* @var string почтовая рассылка, куда входят админы
|
||||||
|
*/
|
||||||
|
private $adminGroup = '# Magic_admins';
|
||||||
|
/**
|
||||||
|
* @var array массив групп, которые не должны участвовать в авторизации пользователя и поэтому могут не храниться
|
||||||
|
*/
|
||||||
|
#Гаврилов
|
||||||
|
//ИСПОЛЬЗУЙ МАССИВ НИЖЕ, ЧТОБЫ УДАЛЯТЬ ГРУППЫ И НЕ ХРАНИТЬ ИХ В ПРОФИЛЕ ПОЛЬЗОВАТЕЛЯ. ИЛИ ЗАБИТЬ ХЕР И ХРАНИТЬ ВСЕ? ТОГДА УДАЛИ МАССИВ НИЖЕ
|
||||||
|
private $unnecessaryGroups = array(
|
||||||
|
'MCO.',
|
||||||
|
'ARSNOVA.',
|
||||||
|
'CHATBOT.',
|
||||||
|
'FOA_PROJECTS.',
|
||||||
|
'MAXOPTRA.',
|
||||||
|
'WEBSIGNER.',
|
||||||
|
'ECM.',
|
||||||
|
'BASIS.',
|
||||||
|
'MCO_DOCUMENT.',
|
||||||
|
'FACTOR.',
|
||||||
|
'BIP.',
|
||||||
|
'NKK.',
|
||||||
|
'CHATS.',
|
||||||
|
'IB_BSC.',
|
||||||
|
'DASHAAI.',
|
||||||
|
'VDI.',
|
||||||
|
'CTX',
|
||||||
|
'AP.AE.',
|
||||||
|
'Way4',
|
||||||
|
'sg.',
|
||||||
|
'AD.TEDDY',
|
||||||
|
'AP.APPV_',
|
||||||
|
'AP.BI_',
|
||||||
|
'AP.Citrix_',
|
||||||
|
'AP.CSD_',
|
||||||
|
'AP.EFK_',
|
||||||
|
'AP.FlexiCapture_',
|
||||||
|
'AP.HPSM.ACCESS.1',
|
||||||
|
'AP.HPSM.ACCESS.3',
|
||||||
|
'AP.HPSM.APPR_BR.Collection.',
|
||||||
|
'AP.HPSM.APPR_BR.CS.',
|
||||||
|
'AP.HPSM.ACCESS.CC',
|
||||||
|
'AP.HPSM.ACCESS.123',
|
||||||
|
'AP.HPSM.ACCESS.1.2',
|
||||||
|
'AP.HPSM.APPR',
|
||||||
|
'AP.OCP',
|
||||||
|
'AP.IBS_',
|
||||||
|
'AP.Intranet_',
|
||||||
|
'AP.Intranet.',
|
||||||
|
'AP.Jenkins_',
|
||||||
|
'AP.Kibana.',
|
||||||
|
'AP.LICA.',
|
||||||
|
'AP.MailSteam.',
|
||||||
|
'AP.MCO_',
|
||||||
|
'AP.MDW.',
|
||||||
|
'AP.POCHTA_',
|
||||||
|
'AP.PREPROD',
|
||||||
|
'AP.Prometheus',
|
||||||
|
'AP.RDS_',
|
||||||
|
'AP.SAS_',
|
||||||
|
'AP.Seguranzza_',
|
||||||
|
'AP.TEST.LICA.',
|
||||||
|
'AP.Test.MCO_',
|
||||||
|
'AP.TEST',
|
||||||
|
'App_',
|
||||||
|
'BTA_',
|
||||||
|
'Calculations ',
|
||||||
|
'Cards_',
|
||||||
|
'CC.',
|
||||||
|
'CCS_',
|
||||||
|
'Citrix',
|
||||||
|
'Collection_HR_',
|
||||||
|
'Collection_All',
|
||||||
|
'CS_',
|
||||||
|
'CSD_',
|
||||||
|
'DA_',
|
||||||
|
'DB.',
|
||||||
|
'DB_',
|
||||||
|
'Deny_',
|
||||||
|
'Diasoft ',
|
||||||
|
'Digital ',
|
||||||
|
'DOR_',
|
||||||
|
'ECM_',
|
||||||
|
'FS.',
|
||||||
|
'FW.',
|
||||||
|
'INFO_',
|
||||||
|
'MDC.',
|
||||||
|
'NRM_Collection',
|
||||||
|
'PFA.',
|
||||||
|
'RenTest_',
|
||||||
|
'ReportingGroup ',
|
||||||
|
'RS.',
|
||||||
|
'SG.',
|
||||||
|
'SP.',
|
||||||
|
'SRV',
|
||||||
|
'SRVFI09_',
|
||||||
|
'SSO_',
|
||||||
|
'test',
|
||||||
|
'TR_',
|
||||||
|
'WWW_',
|
||||||
|
'MailStream',
|
||||||
|
'SRVTST',
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(private AuthorizationService $authorizationService, private ApiResponder $apiResponder) {}
|
||||||
|
|
||||||
|
#Гаврилов
|
||||||
|
//ПЕРЕИМЕНУЙ КОНТРОЛЛЕР НА AUTHcoNTROLLER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод завершения пользовательской сессии
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function logout()
|
||||||
|
{
|
||||||
|
//Удаляем все sanctum токены пользователя. Удаление всех токенов приведет к выходу пользователя со всех устройств, где он был залогинен в Magic
|
||||||
|
User::where('login', UserContext::getUserLogin())->first()->tokens()->delete();
|
||||||
|
session()->invalidate();
|
||||||
|
#Гаврилов
|
||||||
|
//ВЫЗОВ СКРИПТА НА СТАРОМ МЭДЖИКЕ ДЛЯ УДАЛЕНИЯ КУК АУТЕНТИФИКАЦИИ (ЛОГИН И ГРУППЫ)
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#Гаврилов
|
||||||
|
//РАЗНЕСИ ЛОГИКУ МЕЖДУ МЕТОДАМИ, А ТО ПОКА ВСЯ ЛОГИКА В ОДНОМ МЕТОД LDAPCHECK
|
||||||
|
public function ldapCheckUser(Request $request)
|
||||||
|
{
|
||||||
|
if ($request->_auth_login) {
|
||||||
|
if (Auth::attempt([
|
||||||
|
'samaccountname' => $request->_auth_login,
|
||||||
|
'password' => $request->_auth_password,
|
||||||
|
])) {
|
||||||
|
|
||||||
|
$userGroups = $this->getUserGroups($request->_auth_login);
|
||||||
|
session()->put('_auth_login', $request->_auth_login);
|
||||||
|
session()->put('_auth_groups', $userGroups);
|
||||||
|
//Если пользователь зашел впервые - записываем его логин в таблицу users. Она нужна для корректного взаимодействия с пакетом Sanctum
|
||||||
|
$user = User::firstOrCreate(
|
||||||
|
['login' => $request->_auth_login],
|
||||||
|
);
|
||||||
|
//Удаляем все предыдущие sanctum токены пользователя
|
||||||
|
User::where('login', $request->_auth_login)->first()->tokens()->delete();
|
||||||
|
//Определяем на какую страницу нужно бросить пользователя после успешной аутентификации. По умолчанию кидаем в меню
|
||||||
|
$redirectUrl = session()->has('_auth_prev_page') ? session()->get('_auth_prev_page') : ('/menu');
|
||||||
|
$isAdminFlag = in_array($this->adminGroup, $userGroups);
|
||||||
|
//Кладем в сессию информацию о том является ли пользователь админом
|
||||||
|
session()->put('is_admin', $isAdminFlag);
|
||||||
|
//Устанавливаем в пользовательский сервис параметры пользователя
|
||||||
|
UserContext::setUserLogin($request->_auth_login);
|
||||||
|
UserContext::setUserADGroups($userGroups);
|
||||||
|
UserContext::setUserEmails($userGroups);
|
||||||
|
UserContext::setIsAdminFlag($isAdminFlag);
|
||||||
|
$userPermissions = $this->authorizationService->getUserAppPermissions();
|
||||||
|
UserContext::setUserAppPermissions($userPermissions);
|
||||||
|
//Генерим Sanctum токен, чтобы поместить его в куки при редиректе
|
||||||
|
$token = $user->createToken('sanctum-token', [
|
||||||
|
'permissions' => $userPermissions
|
||||||
|
//Устанавливаем время жизни sanctum токена синхронно с временем жизни сессии из конфига
|
||||||
|
], now()->addHours(config('app.session_life_time') / 60))->plainTextToken;
|
||||||
|
return redirect($redirectUrl)
|
||||||
|
->withCookie('sanctum_token', $token, 60 * 24, '/', null, true, true);
|
||||||
|
} else {
|
||||||
|
#Гаврилов
|
||||||
|
//СООБЩЕНИЕ ОБ ОШИБКЕ ПРИ НЕУДАЧНОЙ АУТЕНТИФИКАЦИИ
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод фонового обновления санктум токена при получении 401 ошибки в ответе api ендпоинта
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function silentRefreshUserSanctumToken(Request $request)
|
||||||
|
{
|
||||||
|
//Если сессия истекла - возвращаем 401 ошибку и редирект на /login в axios
|
||||||
|
if (!Auth::check()) {
|
||||||
|
$this->apiResponder->setDto(new ApiResponseDto(null, ['token_refresh' => false]));
|
||||||
|
return response()->json($this->apiResponder->error(), 401);
|
||||||
|
}
|
||||||
|
$token = $request->cookie('sanctum_token');
|
||||||
|
$accessToken = \Laravel\Sanctum\PersonalAccessToken::findToken($token);
|
||||||
|
//Если токен "протух" - продлеваем его на час
|
||||||
|
if (now()->diffInMinutes($accessToken->expires_at, false) < 1) {
|
||||||
|
$accessToken->update(
|
||||||
|
[
|
||||||
|
'expires_at' => now()->addHours(1)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->apiResponder->setDto(new ApiResponseDto(null, ['token_refresh' => true]));
|
||||||
|
return response()->json($this->apiResponder->success());
|
||||||
|
} else {
|
||||||
|
//Если токен еще "свежий" - значит причина 401 ошибки в чем-то другом. Не обновляем токен, просто возвращаем 401 ошибку и редирект на /login в axios
|
||||||
|
$this->apiResponder->setDto(new ApiResponseDto(null, ['token_refresh' => false]));
|
||||||
|
return response()->json($this->apiResponder->error(), 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод получает группы пользователя из ldap
|
||||||
|
*
|
||||||
|
* @param string $userLogin логин пользователя, чьи группы получаем
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getUserGroups($userLogin)
|
||||||
|
{
|
||||||
|
$userGroups = [];
|
||||||
|
$ldapUser = LdapUserInfo::findBy('samaccountname', $userLogin);
|
||||||
|
$ldapUser->memberOf;
|
||||||
|
if (isset($ldapUser->memberOf)) {
|
||||||
|
foreach ($ldapUser->memberOf as $ldapGroupInfo) {
|
||||||
|
$CN_group = substr($ldapGroupInfo, 0, stripos($ldapGroupInfo, ","));
|
||||||
|
$groupName = str_replace(array('CN=', '\\'), array('', ''), $CN_group);
|
||||||
|
$clearGroupName = trim($groupName);
|
||||||
|
if ($clearGroupName && $clearGroupName == str_replace($this->unnecessaryGroups, '', $clearGroupName)) {
|
||||||
|
$userGroups[] = $clearGroupName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $userGroups;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use App\Facades\UserContext;
|
||||||
|
use App\Models\User;
|
||||||
|
//очереди для чтения нотификаций
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Laravel\Sanctum\PersonalAccessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Глобальный посредник аутентификации на платформе Magic для всех роутов платформы
|
||||||
|
*/
|
||||||
|
class AuthenticateMagic
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
|
||||||
|
//TODO ПРОВЕРИТЬ
|
||||||
|
//Если сессия не стартовала возможно пользователь сразу обращается к api ендпоинту (ПРОВЕРИТЬ)
|
||||||
|
if (session()->isStarted()) {
|
||||||
|
if (session()->has('_auth_login')) {
|
||||||
|
//гаврилов. получение токена
|
||||||
|
// $token = $request->user();
|
||||||
|
// $userId = User::where('login', $token->getAttributes()['samaccountname'][0])->get()->toArray()[0]['id'];
|
||||||
|
// $tokenExpires = PersonalAccessToken::where('tokenable_id', $userId)->get()->toArray()[0]['expires_at'];
|
||||||
|
// echo '<pre>'; var_dump(new \DateTime($tokenExpires)); echo'</pre>';
|
||||||
|
// echo '<pre>'; var_dump(PersonalAccessToken::where('tokenable_id', $userId)->get()->toArray()[0]['expires_at']); echo'</pre>';
|
||||||
|
//Если токен истекает менее через 60 минут, продлеваем его на 2 часа. Ситуации, что сессия протухла, а токен продолжает жить не может случиться, так как апи запросы с фронта отправляют куку аутентификации, которая проверяется при $this->authenticate. Если она протухла, возвратится 401 ошибку.
|
||||||
|
// if ($token->expires_at->diffInMinutes(now()) < 60) {
|
||||||
|
// $token->update(
|
||||||
|
// [
|
||||||
|
// 'expires_at' => now()->addHours(2)
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
//Через фасад устанавливаем все значения аутентифицированного пользователя, чтобы через этот же фасад можно было и любого места в приложении их получить.
|
||||||
|
UserContext::setUserLogin(session()->get('_auth_login'));
|
||||||
|
$userGroups = session()->get('_auth_groups');
|
||||||
|
UserContext::setUserADGroups($userGroups);
|
||||||
|
UserContext::setUserEmails($userGroups);
|
||||||
|
UserContext::setIsAdminFlag(session()->get('is_admin'));
|
||||||
|
//На этапе посредника мы не проводим повторное определение ролей пользователя, это было сделано в LoginController в процессе авторизации после успешной аутентификации. Здесь мы уже берем его доступы из таблицы с токенами
|
||||||
|
$user = User::where('login', UserContext::getUserLogin())->first();
|
||||||
|
UserContext::setUserId($user->id);
|
||||||
|
UserContext::setUserAppPermissions($user->tokens()->latest()->first()->abilities['permissions']);
|
||||||
|
#Гаврилов
|
||||||
|
//Насксолько я помню, это связано с механизмом получения нотификаций на фронте (через отдельный компонент React) на случай, если нотификации формируются на бэке и должны читаться фронтом сразу при рендеринге. Обычно, нотификации формируются после запроса с фронта, например, при fetch запросе на отправку заявки на такси и сразу же рендерятся на этой же странице после выполнения fetch запроса, но бывают ситуации, когда пользователя с бэке редиректит на другую страницу, в результате чего тяряется "контекст" нотификаций. Я как-то настраивал чтение редис очередей на любой странице, чтобы при рендеринге любой страницы сразу проверялась есть ли непрочитенная нотификация. Если есть - она читается, отображается и удаляется из очереди. Но может конкретно строка ниже связана с тестированием , уже не помню
|
||||||
|
Redis::setex('notifications', 60, 123);
|
||||||
|
return $next($request);
|
||||||
|
} else {
|
||||||
|
//Получаем адрес предыдущей страницы, на которую хотел попасть пользователь, чтобы направить его после успешной аутентификации на этот адрес
|
||||||
|
$prevPageUrl = explode('/', $_SERVER['REDIRECT_URL']);
|
||||||
|
//Удаляем из URL редиректа пустые сегменты и сегмент с названием приложения (оно подставляется при редиректе само)
|
||||||
|
unset($prevPageUrl[0], $prevPageUrl[1]);
|
||||||
|
//Кладем в сессию адрес страницы. только если это не страница выхода, иначе после аутентификации пользователя сразу выбросит
|
||||||
|
session()->put('_auth_prev_page', implode('/', $prevPageUrl) == 'logout' ? '/menu' : implode('/', $prevPageUrl));
|
||||||
|
return redirect('/login');
|
||||||
|
//редирект на страницу login с сообщением об ошибке
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return redirect('/login');
|
||||||
|
//redirect на страницу login после которой точно сессия застартует, так как это webроут, а не api
|
||||||
|
}
|
||||||
|
// return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user