RMS*_*ira 4 xamarin.android async-await xamarin.forms chrome-custom-tabs identityserver4
目前我正在开发一个 Xamarin 应用程序,它使用 IdentityModel.OidcClient 对我的服务器进行身份验证,并且正在使用文档(https://github.com/IdentityModel/IdentityModel.OidcClient2)中提供的自动模式完成。一切正常var result = await client.LoginAsync();
,使用 AccessToken 返回 LoginResult 等。
我想弄清楚的是,应如何处理后退按钮、最近的应用程序按钮(均在 android 上)和 ChromeCustomTabsBrowser 上的关闭按钮,因为这三个操作会关闭附加到 oidcClient 的 Ibrowser 而不返回响应,并且会让我卡住等待响应阻止我处理其余的代码验证。
private async Task SignInAsync() {
IsBusy = true;
await Task.Delay(500);
try {
LoginResult result = await IdentityService.LoginAsync(new LoginRequest());
if (result == null) {
OnError(noInternetErrorMessage);
IsBusy = false;
return;
}
if (result.IsError) {
OnError(result.Error);
} else {
string userName = result.User.Claims.Where(claim => claim.Type == userNameClaimType).Select(claim => claim.Value).SingleOrDefault();
_UserToken = IdentityService.CreateOrUpdateUserToken(userName, result);
if (_UserToken != null) {
await NavigationService.NavigateToAsync<LockScreenViewModel>();
} else {
OnError(errorMessage);
}
}
} catch (Exception e) {
OnError(e.ToString());
}
IsBusy = false;
}
Run Code Online (Sandbox Code Playgroud)
在前面的代码块中,我无法访问if (result == null)这些按钮是否被点击,这反过来又会阻止我删除 loginView 中的 ActivityIndicator 并向用户提供登录按钮,以便他可以再次尝试登录。
发生这种情况是因为您的IdentityService.LoginAsync()任务实际上仍在后台等待自定义选项卡活动回调发生,而不管自定义选项卡浏览器不再可见。由于用户在完成登录往返之前关闭,因此不会触发回调,直到用户在以后的尝试中完成往返。每次登录尝试都会创建一个新的等待任务,因此每次用户过早关闭自定义选项卡窗口时,等待任务的集合都会增加。
在用户实际完成登录往返时,很明显所有任务仍在等待,因为当等待已久的回调最终发生时,它们都会立即解冻。这带来了另一个需要处理的问题,因为除了最后一个任务之外的所有任务都会导致'invalid state'oidc 错误结果。
我通过在开始新的登录尝试之前取消上一个任务来解决这个问题。我在自定义界面上添加了一个TryCancel方法。在实现中,保留了对要返回的的引用。下次用户单击登录按钮时,将首先调用 before以使用保留的引用解锁仍在等待的上一次登录尝试。ChromeCustomTabsBrowserIBrowserExtraChromeCustomTabsBrowser.InvokeAsyncTaskCompletionSourceTryCancelChromeCustomTabsBrowser.LoginAsync
为了使这项工作,IsBusy=True应该推迟到自定义选项卡回调之后(自定义选项卡浏览器无论如何都将在顶部),以在单击自定义选项卡关闭按钮时保持 gui 交互性。否则用户将永远无法重新尝试登录。
更新:根据要求添加了示例代码。
public interface IBrowserExtra
{
void TryCancel();
}
public class ChromeCustomTabsBrowser : IBrowser, IBrowserExtra, IBrowserFallback
{
private readonly Activity _context;
private readonly CustomTabsActivityManager _manager;
private TaskCompletionSource<BrowserResult> _task;
private Action<string> _callback;
public ChromeCustomTabsBrowser()
{
_context = CrossCurrentActivity.Current.Activity;
_manager = new CustomTabsActivityManager(_context);
}
public Task<BrowserResult> InvokeAsync(BrowserOptions options)
{
var builder = new CustomTabsIntent.Builder(_manager.Session)
.SetToolbarColor(Color.Argb(255, 0, 0, 0))
.SetShowTitle(false)
.EnableUrlBarHiding()
.SetStartAnimations(_context, Android.Resource.Animation.SlideInLeft, Android.Resource.Animation.SlideOutRight)
.SetExitAnimations(_context, Android.Resource.Animation.SlideInLeft, Android.Resource.Animation.SlideOutRight);
var customTabsIntent = builder.Build();
// ensures the intent is not kept in the history stack, which makes
// sure navigating away from it will close it
customTabsIntent.Intent.AddFlags(ActivityFlags.NoHistory);
_callback = null;
_callback = url =>
{
UnsubscribeFromCallback();
_task.TrySetResult(new BrowserResult()
{
Response = url
});
};
SubscribeToCallback();
// Keep track of this task to be able to refer it from TryCancel later
_task = new TaskCompletionSource<BrowserResult>();
customTabsIntent.LaunchUrl(_context, Android.Net.Uri.Parse(options.StartUrl));
return _task.Task;
}
private void SubscribeToCallback()
{
OidcCallbackActivity.Callbacks += _callback;
}
private void UnsubscribeFromCallback()
{
OidcCallbackActivity.Callbacks -= _callback;
_callback = null;
}
void IBrowserExtra.TryCancel()
{
if (_callback != null)
{
UnsubscribeFromCallback();
}
if (_task != null)
{
_task.TrySetCanceled();
_task = null;
}
}
}
public class LoginService
{
private static OidcClient s_loginClient;
private Task<LoginResult> _loginChallengeTask;
private readonly IBrowser _browser;
private readonly IAppInfo _appInfo;
public LoginService(
IBrowser secureBrowser,
IBrowserFallback fallbackBrowser,
IAppInfo appInfo)
{
_appInfo = appInfo;
_browser = ChooseBrowser(appInfo, secureBrowser, fallbackBrowser);
}
private IBrowser ChooseBrowser(IAppInfo appInfo, IBrowser secureBrowser, IBrowserFallback fallbackBrowser)
{
return appInfo.PlatformSupportsSecureBrowserSession ? secureBrowser : fallbackBrowser as IBrowser;
}
public async Task<bool> StartLoginChallenge()
{
// Cancel any pending browser invocation task
EnsureNoLoginChallengeActive();
s_loginClient = OpenIdConnect.CreateOidcClient(_browser, _appInfo);
try
{
_loginChallengeTask = s_loginClient.LoginAsync(new LoginRequest()
{
FrontChannelExtraParameters = OpenIdConnectConfiguration.LoginExtraParams
});
// This triggers the custom tabs browser login session
var oidcResult = await _loginChallengeTask;
if (_loginChallengeTask.IsCanceled)
{
// task can be cancelled if a second login attempt was completed while the first
// was cancelled prematurely even before the browser view appeared.
return false;
}
else
{
// at this point we returned from the browser login session
if (oidcResult?.IsError ?? throw new LoginException("oidcResult is null."))
{
if (oidcResult.Error == "UserCancel")
{
// Graceful exit: user canceled using the close button on the browser view.
return false;
}
else
{
throw new LoginException(oidcResult.Error);
}
}
}
// we get here if browser session just popped and navigation is back at customer page
PerformPostLoginOperations(oidcResult);
return true;
}
catch (TaskCanceledException)
{
// swallow cancel exception.
// this can occur when user canceled browser session and restarted.
// Previous session is forcefully canceled at start of ExecuteLoginChallenge cauing this exception.
LogHelper.Debug($"'Login attempt was via browser roundtrip canceled.");
return false;
}
}
private void EnsureNoLoginChallengeActive()
{
if (IsLoginSessionStarted)
{
(_browser as IBrowserExtra)?.TryCancel();
}
}
private static bool IsLoginSessionStarted => s_loginClient != null;
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
2599 次 |
| 最近记录: |