Когда токен доступа недоступен, можете ли вы придумать какой-нибудь элегантный способ сделать перенаправление на интерактивный логин всякий раз, когда выбрасывается AccessTokenNotAvailableException
, в одном месте? Или, что еще лучше, есть ли способ узнать, когда токен недействителен или недоступен, несмотря на AuthenticationState.User.Identity?.IsAuthenticated == true
, чтобы я мог избежать попытки в этих случаях?
Подробности:
Использование MSAL для аутентификации в Blazor WebAssembly: когда я использую любую из различных функций HttpClient.Get*Async, я настраиваю именованный клиент, который всегда будет присоединять токен Bearer к исходящим запросам API.
В клиентской Program.cs:
builder.Services.AddHttpClient("Authenticated", client =>
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
В CustomAuthorizationMessageHandler.cs:
/// <summary>
/// Customer handler for Authorization that is necessary when submitting REST API calls to end points that
/// are different then you base address for your application.
/// Information on this topic can be found at
/// https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios </summary>
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager, IConfiguration Configuration)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { $"{Configuration.GetRequired("ServiceUrl")}" }
//,
//scopes: new[] { "User.Read", "User.Write" }
);
}
}
Поэтому в моем коде, когда AuthenticationState.User.Identity?.IsAuthenticated
имеет значение false или null, я могу избежать попытки HTTP-вызова. Но когда AuthenticationState.User.Identity?.IsAuthenticated == true
, пользователь может иногда получать исключения AccessTokenNotAvailableException в этом процессе, например, когда сеанс входа пользователя недействителен или токены были удалены вручную в браузере.
Поэтому большую часть времени я жду таких запросов, и я написал вспомогательный метод расширения, который я могу связать с вызовом await
ed, который будет перехватывать любое исключение в ожидании, а затем перенаправлять на интерактивные страницы входа в случае необходимости, чтобы пользователь имеет хороший опыт повторного входа в систему по мере необходимости, вместо того, чтобы видеть «сломанную страницу» из-за необработанных исключений AccessTokenNotAvailableException.
public static async Task<T?> CheckForTokenUnavailable<T>(this Task<T?> task)
where T : class
{
try
{
return await task;
}
catch (AccessTokenNotAvailableException e)
{
e.Redirect(); // starts interactive sign-in
return null;
}
}
Теперь я хочу вызывать это только тогда, когда HTTP-запросы ожидаются в любом компоненте графического интерфейса. Если бы я вызывал его из запроса XHR, который инициируется каким-либо другим способом, например, из обработчиков событий MSAL или где-то в этом роде, результирующее перенаправление на интерактивный вход в систему было бы поглощено контекстом XHR и привело бы к ошибкам CORS.
Так или иначе, проблема в том, что я должен вызывать это везде, где я делаю запрос HttpClient из компонента. В сотнях мест я вручную привязываю метод CheckForTokenUnavailable к любому клиентскому вызову, который мне нужно сделать (который варьируется), например:
if (AuthenticationState.User.Identity?.IsAuthenticated == true){
string url = $"{ServiceUrl}/items/{itemId}/childIds";
var client = ClientFactory.CreateClient("Authenticated");
return await client.GetFromJsonAsync<List<int>>(url).CheckForTokenUnavailable();
}
Сегодня я трачу кучу времени, пытаясь сделать это с помощью ErrorBoundary. Я унаследовал от ErrorBoundary и переопределил OnErrorAsync для поиска AccessTokenNotAvailableException, но в ErrorBoundary должен быть какой-то нюанс, которого я не понимаю, потому что он, кажется, делает то, что я хочу, но затем никогда не перенаправляет обратно вызывающему абоненту — вместо этого В итоге я застреваю либо на исходной странице с большим количеством исключений AccessTokenNotAvailableException, либо вижу, что страница ErrorContent ничего не делает. Необходимо разрешить клиенту MSAL перенаправить обратно вызывающему объекту и повторить попытку, но я не могу этого сделать. Хотя я не совсем понимаю, почему это так, теперь я подозреваю, что ErrorBoundary — это неправильный механизм для попытки добиться вызова e.Redirect, когда это необходимо. Поэтому я не включаю свой код ErrorBoundary, так как думаю, что это будет лаять не на то дерево, и вместо этого я задам свои вопросы более высокого уровня, выделенные жирным шрифтом выше — дайте мне знать, если вы думаете, что ErrorBoundary — это мой лучший выбор, и я могу пересмотрите это, хотя я сомневаюсь в этом.
Похоже, общая рекомендация состоит в том, чтобы оборачивать вызовы с помощью try
/catch
и после перехвата AccessTokenNotAvailableException ex
выполнять перенаправление с помощью ex.Redirect()
. Я считаю, что это ужасный подход, и я все еще ищу что-то лучшее.
Чтобы избежать получения AccessTokenNotAvailableException, я теперь сначала проверяю токен перед всеми попытками его использования. Для моего сайта Blazor WebAssembly это означает поставить галочку в моем основном компоненте макета и отображать @body только после того, как он вернет значение true; если false, перенаправить на интерактивный вход. Если я это сделаю, я не получу AccessTokenNotAvailableException при первой попытке API.
@inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> OptionsSnapshot
/* ... */
@code {
private bool ShowBody { get; set; } = false;
protected override async Task OnInitializedAsync()
{
if ((await AuthenticationStateTask).User?.Identity?.IsAuthenticated == true)
{
// The check for access token needs to be done in this file 'MainLayout.razor' because, we need to be successfully
// authenticated on the page before we can test to see if our access token for our main data API
// is still valid. We want to make this call before anything else tries to access the service for a call that requires
// user credentials.
ShowBody = await HasValidAccessToken();
StateHasChanged();
}
}
public async Task<bool> HasValidAccessToken()
{
if (NavigationManager.Uri.Contains("login-callback"))
{
// We get here after the login redirect returns to our site.
// MSAL saving the token is still in progress. Not sure why. Workaround is to delay.
await Task.Delay(1000); // 1000ms seems fine; 500 ms wasn't enough.
}
AccessTokenResult tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out AccessToken? token))
{
return true;
}
else
{
NavigationManager.NavigateToParagonSignIn(OptionsSnapshot);
return false;
}
}
}
Для меня мой основной макет — это макет, который я использую для всех компонентов, которым требуется аутентификация, чтобы они вообще что-либо отображали. Другие макеты предназначены для компонентов, которым не требуется аутентификация (особенно сама страница аутентификации — та, на которой есть компонент <RemoteAuthenticatorView Action = "@Action" />
). Если вы делаете свои макеты по-другому, вам придется критически подумать, где поставить такую галочку.
Это решает проблему только тогда, когда она может возникнуть во время загрузки приложения, что было большей частью моей проблемы.
Крис Сэйнти упоминает здесь, как их избегать, но только для неавторизованных запросов. chrissainty.com/… Я хочу сделать это для аутентифицированных запросов.