Кастомная авторизация в ASP.net Blazor не работает
Доброго времени суток.
Делал свою авторизацию для веб приложения на ASP.NET Blazor и столкнулся с проблемой.
Работает/не работает это сейчас так:
- пользователь вводит логин/пароль;
- мы ищем в БД такую пару. Если нет, то ничего, просто пишем пользователю что он не залогинился. Если есть - переадресовываем его на index, а в куки даём ему уникальный ключик, с которым ассоциируем его уровень доступа;
- при каждом GET запросе берём этот ключик, и сравниваем с сохранёнными активными сессиями, и если есть сессия с таким ключом то определяем по нему уровень доступа и сохраняем его в Singleton такого вида:
public class AppData
{
public string user { get; set; }
public DBUser currentUser { get; set; }
}
Вот такой GET запрос:
public void OnGet() {
var userId = HttpContext.Request.Cookies["user"];
if (string.IsNullOrEmpty(userId)) {
userId = -1 + "";
}
appData.user = userId;
appData.currentUser = UsersHandler.getUserFromCacheById(userId);
DBHandler.errors.Add("get as " + appData.currentUser.login);
}
Если нет такой сохранённой сессии, то отправляем пользователя на шаг 1;
- при нажатии кнопки разлогиниться - удаляем сессию.
Может я конечно изобрёл дурь какую, прошу сильно не ругать. Но мне очень хотелось использовать существующую таблицу с пользователями.
А теперь собственно в чём сама проблема. Оказалось что GET запрос отправляется не при каждом переходе между страницами, а только когда нажимается кнопка "обновить", или открывается ранее не открывавшаяся страница. Видимо так экономятся ресурсы сервера. Но из за этого у меня возникает ситуация, когда пользователь u1 зашёл с ролью r1, а пользователь u2 зашёл как r2. После того как, например u1 перейдёт на другую страницу с выполнением GET запроса, обновив currentUser и user в AppData, у u2, пока тот не отправит GET запрос, всё выглядит так, будто роль у него не r2, а r1.
У меня была идея отправлять GET запросы при любой перезагрузке страницы, но я не нашёл способа. Ну да и выглядит это как костыль. Здорово было бы иметь возможность хранить для каждой сессии экземпляр DBUser в отдельном месте. Но я совершенно не понимаю как этого добиться.
Я сперва думал что singleton сервис для каждой сессии свой, оказалось что он общий на всё приложение. То же самое со static классами.
Есть вообще в приложении такое место где бы можно было хранить, для каждого пользователя свой экземпляр класса DBUser, ну или хотя-бы как string ключ user?
Я даже доступ к куки могу получить только в GET запросе, или через IJSRuntime, но уже после отрисовки. А после отрисовки он мне уже не нужен...
У меня уже нет никаких идей, подскажите пожалуйста.
Ответы (1 шт):
Спасибо tym32167 за подсказку.
Всё нижесказанное относится только к ServerSide Blazor. Для WebAssembly, по приведённой ниже ссылке, есть соответствующая инструкция.
Мне удалось найти способ хранить ключ пользователя для каждого пользователя отдельно. Реализовал я это с помощью ProtectedSessionStorage и каскадных параметров.
Сперва я создал компонент - поставщик ключа сессии пользователя (CommonVariablesComponent.razor).
Его содержимое:
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@inject ProtectedLocalStorage ProtectedLocalStorage
@if (isLoaded)
{
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
}
else
{
<LoaderAnimation/>
}
@code {
private bool isLoaded = false;
[Parameter]
public RenderFragment ChildContent { get; set; }
public string currentUserToken { get; set; }
public async Task setCurrentUser(DBUser user)
{
currentUserToken = user.identificator;
await SaveChangesAsync();
}
protected override async Task OnInitializedAsync()
{
ProtectedBrowserStorageResult<string> tokenResult = await ProtectedLocalStorage.GetAsync<string>("token");
currentUserToken = tokenResult.Success ? tokenResult.Value : "-1";
isLoaded = true;
}
public async Task SaveChangesAsync()
{
await ProtectedLocalStorage.SetAsync("token", currentUserToken);
}
}
Важным нюансом, чтобы это всё работало является то, что нужно отключить пререндер в приложении заменив в _Hosts.cshtml
<app> <component type="typeof(App)" render-mode="ServerPrerendered" /> </app>на
<app> <component type="typeof(App)" render-mode="Server" /> </app>Это позволит использовать interop в методе OnInitialized(), что необходимо здесь, для ProtectedSessionStorage, и чего мне так не хватало для сохранения ключа в Cookies.
Тем не менее я остановился именно на ProtectedSessionStorage, отказавшись от Cookies, т.к. оно предоставляет шифрование данных из коробки, что лишает нас риска того, что пользователь напрямую считает наши данные.
Далее мы в компонент CommonVariablesComponent оборачиваем следующим образом всё наше приложение в App.razor:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<CommonVariablesComponent>
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</CommonVariablesComponent>
</Found>
<NotFound>
<CommonVariablesComponent>
<LayoutView Layout="@typeof(MainLayout)">
<p>Неправильная ссылка.</p>
</LayoutView>
</CommonVariablesComponent>
</NotFound>
</Router>
Таким образом передавая Value во все вложенные компоненты. В Value, как видно выше, мы передаём ссылку на компонент CommonVariablesComponent, таким образом информируя вложенные компоненты о всех публичных полях и методах этого компонента. В.т.ч. о поле currentUserToken, к которому мы теперь можем получить доступ из любой точки приложения, обратившись к CommonVariablesComponent как к каскадному параметру:
[CascadingParameter] private CommonVariablesComponent CommonVariables { get; set; }
и дальше я делаю то о чём мечтал - достаю текущего пользователя по его токену/ключу:
currentUser = UsersHandler.getUserFromCacheById(CommonVariables.currentUserToken);
В общем свой велосипед я доделал. И им вполне доволен. Спасибо всем.