From 4dc1c09693dc6d781d3f5c8ef52389b167984cbc Mon Sep 17 00:00:00 2001 From: vasya Date: Fri, 13 Mar 2026 18:45:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B0=D1=83=D1=82=D0=B5=D0=BD=D1=82=D0=B8=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=81=D1=80=D0=B0=D0=B1=D0=B0=D1=82=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=B2?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8F=20(=D0=B4=D0=B0,=20?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=BD=D0=B0=D1=8E,=20=D1=87=D1=82=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B0=D0=B4?= =?UTF-8?q?=D0=BE=20=D0=B1=D1=8B=D0=BB=D0=BE=20=D0=BF=D0=BE=20=D0=B4=D1=80?= =?UTF-8?q?=D1=83=D0=B3=D0=BE=D0=BC=D1=83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/LoginController.php | 246 ++++++++++++++++++++++ app/Http/Middleware/AuthenticateMagic.php | 77 +++++++ 2 files changed, 323 insertions(+) create mode 100644 app/Http/Controllers/LoginController.php create mode 100644 app/Http/Middleware/AuthenticateMagic.php diff --git a/app/Http/Controllers/LoginController.php b/app/Http/Controllers/LoginController.php new file mode 100644 index 0000000..b3a149e --- /dev/null +++ b/app/Http/Controllers/LoginController.php @@ -0,0 +1,246 @@ +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; + } +} diff --git a/app/Http/Middleware/AuthenticateMagic.php b/app/Http/Middleware/AuthenticateMagic.php new file mode 100644 index 0000000..c3a6c08 --- /dev/null +++ b/app/Http/Middleware/AuthenticateMagic.php @@ -0,0 +1,77 @@ +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 '
'; var_dump(new \DateTime($tokenExpires)); echo'
'; + // echo '
'; var_dump(PersonalAccessToken::where('tokenable_id', $userId)->get()->toArray()[0]['expires_at']); echo'
'; + //Если токен истекает менее через 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); + } +}