User 10778296 Ответов: 3

Установите CurrentPrincipal в асинхронном входе в систему в WPF


Я искал это в интернете и не мог найти действительно работающего решения. Ситуация выглядит следующим образом: у меня есть приложение WPF, где я хочу представить пользователю простую форму входа в систему. Пытаюсь работать с MVVM, поэтому у меня есть LoginViewModel со следующим кодом за командой login:

try
            {
                WithClient(servfact.GetServiceClient<IAccountService>(), proxy =>
                {
                    principal = proxy.AuthenticateUser(Login, password);
                });
                Thread.CurrentPrincipal = principal;
            }
            catch(...) { ... }


"WithClient" - это короткий метод в моем базовом классе viewmodel, который я использую для создания экземпляра и удаления моих прокси-серверов службы:

protected void WithClient<T>(T proxy, Action<T> codeToExecute)
{
    try { codeToExecute(proxy); }
    finally
    {
        IDisposable toDispose = (proxy as IDisposable);
        if(toDispose != null) { toDispose.Dispose(); }
    }
}


Теперь большинство моих сервисов асинхронны, и у меня есть асинхронный вариант WithClient, который также отлично работает:

protected async Task WithClientAsync<T>(T proxy, Func<T, Task> codeToExecute)
{
    try { await codeToExecute(proxy); }
    finally
    {
        IDisposable toDispose = (proxy as IDisposable);
        if(toDispose != null) { toDispose.Dispose(); }
    }
}


The trouble begins whenever I also want to do the login asynchronously. Obviously I don't want the UI to freeze up as I do the login (or visit any WCF service for that matter). That in itself is working fine, but the problem sits in the piece of code where I set the CurrentPrincipal. This problem is probably familiar to most of you: it seems to set it just fine. Then in my program I want to use the CurrentPrincipal (either on the client side or to send the users login to a WCF service in a messageheader), but it seems to be reset to a standard GenericPrincipal. When I revert the login back to being synchronous, the CurrentPrincipal is just fine. So in short: how do I set the principal in the asynchronous code, having it persist later on, instead of reverting back to a standard principal?

3 Ответов

Рейтинг:
22

Richard Deeming

Это очень интересная проблема. :)

Моей первой мыслью было, что Thread.CurrentPrincipal зависит от текущего потока, а также от async метод в конечном итоге установит его на неправильный поток. Однако, по-видимому, это свойство связано с логический контекст вызова, который правильно протекает через потоки в async метод.

Тем не менее, кажется, что любые изменения, которые вы вносите в контекст логического вызова в рамках async метод отбрасывается, когда метод завершается. Это относится только к .NET 4.5; любой код, предназначенный для .NET 4.0 и использующий Microsoft.Bcl.Async библиотека будет нет проявите такое поведение!

У Стивена Клири есть ответ на StackOverflow[^] в которой обсуждается эта проблема в связи с ASP.NET, и блог[^] что связано с этой проблемой:


В .NET 4.5 асинхронные методы взаимодействуют с логическим контекстом вызова, так что он будет более правильно протекать с асинхронными методами. .. В .NET 4.5 в начале каждого асинхронного метода он активирует поведение "копирование при записи" для своего логического контекста вызова. Когда (если) контекст логического вызова изменяется, он сначала создает локальную копию самого себя.


К сожалению, похоже, что нет никакого способа изменить это поведение. Как только async метод возвращает, любые изменения в CurrentPrincipal имущество будет потеряно.

Вы вероятно быть в состоянии обойти эту проблему с помощью то AppDomain.CurrentDomain.SetThreadPrincipal метод[^Однако этот метод может быть вызван только один раз в течение жизни объекта. AppDomain. Если ваше приложение позволяет пользователям выходить из системы, а затем снова входить в нее, вам нужно будет использовать пользовательский интерфейс. IPrincipal класс, чтобы действовать как фасад к реальному IPrincipal:
public sealed class CurrentPrincipalFacade : IPrincipal
{
    private static readonly CurrentPrincipalFacade _instance = new CurrentPrincipalFacade();
    
    private CurrentPrincipalFacade()
    {
    }
    
    public static CurrentPrincipalFacade Instance
    {
        get { return _instance; }
    }
    
    public IPrincipal WrappedValue { get; set; }
    
    public IIdentity Identity
    {
        get { return WrappedValue == null ? null : WrappedValue.Identity; }
    }
    
    public bool IsInRole(string role)
    {
        return WrappedValue != null && WrappedValue.IsInRole(role);
    }
}

// Startup code:
static void Main()
{
    AppDomain.CurrentDomain.SetThreadPrincipal(CurrentPrincipalFacade.Instance);
    ...
}

// Login:
await WithClientAsync(servfact.GetServiceClient<IAccountService>(), proxy =>
{
    CurrentPrincipalFacade.Instance.WrappedValue = proxy.AuthenticateUser(Login, password);
});


Рейтинг:
1

User 10778296

Ричард, спасибо за ответ. Ну, по крайней мере, я рад, что это не только я ;) Я много искал, и хотя я нашел некоторые решения для ASP.NET (в частности, тот, который предложил Стивен Клири), я действительно не нашел ничего удовлетворяющего для WPF.

Изначально я тоже пошел по маршруту AppDomain, но, как вы сами сказали, мое приложение действительно должно иметь возможность входить и выходить из системы, так что об этом в принципе не может быть и речи как о простом решении.

What I decided to try (and it works, although the 'proper' way of using the Thread.CurrentPrincipal would have been nice, after all it's made for it), was the following: I have a ObjectBase class, which I used as the baseclass for all my client-side entities. In that class I already have a public static CompositionContainer object, where I store all catalogs I need for dependency injection through MEF. I just included a public static ClaimsPrincipal object as well, and when I log in, I store the resulting ClaimsPrincipal that my authentication call returns in there. Upon loggin out, I simply clear that object. Seems to work just fine, as you'd expect.

Так что теперь мой логин выглядит примерно так:

await WithClientAsync(servfact.GetServiceClient<iaccountservice>(), async proxy =>
{
    ObjectBase.ClaimsPrincipal = await proxy.AuthenticateUserAsync(Login, Hasher.CalculateHash(password, Login));
});


После проверки я, вероятно, очень быстро изменю свой код и использую ваше решение, так как оно гораздо лучше отделяет проблему. Еще раз спасибо за это!

Просто для полноты картины: я опустил некоторые несущественные вещи из кода, такие как шифрование пароля и все такое.

Итак, в целом проблема устранена, но я все еще надеюсь, что будущий релиз исправит это. Есть какие-нибудь идеи о том, как .NET 4.6 ведет себя в этом сценарии?


Richard Deeming

Я еще не пробовал 4.6, но сомневаюсь, что поведение изменилось с 4.5; это почти наверняка будет "прорывное" изменение.

[no name]

Как и ожидалось. В любом случае, еще раз спасибо за помощь!

Рейтинг:
0

Xequence

Если вы хотите, чтобы поток сохранялся в новых окнах, вы можете настроить его следующим образом. Я видел, что был ответ, который содержал больше пользовательского кода, когда реальность такова, что объекты уже существуют

string[] = список ролей {"a", "b", "c"}
"имя" = первичный ключ из базы данных, содержащий уникальный идентификатор

домен приложений.CurrentDomain.SetThreadPrincipal(новый GenericPrincipal(нового созданного объекта genericidentity("наименование", класса windowsidentity.GetCurrent().AuthenticationType), строка[]);

Я надеюсь, что это кому-то поможет, и благодарю вас за чтение.


[no name]

Серьезно, этот вопрос был задан 4 года назад... Все двинулись дальше я надеюсь ;-)

Xequence

Я просто хотела выглядеть круто...

Janis Pütz

Какая разница, когда был задан этот вопрос?