在WPF中的异步登录中设置CurrentPrincipal [英] Set CurrentPrincipal in an asynchronous login in WPF

查看:62
本文介绍了在WPF中的异步登录中设置CurrentPrincipal的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在网上搜索这个,并且找不到真正有效的解决方案。情况如下:我有一个WPF应用程序,我想向用户呈现一个简单的登录表单。尝试使用MVVM,所以我在登录命令后面有一个带有以下代码的LoginViewModel:



  try  
{
WithClient(servfact.GetServiceClient< IAccountService>(),proxy = >
{
principal = proxy.AuthenticateUser(登录名,密码);
});
Thread.CurrentPrincipal = principal;
}
catch (...){...}





WithClient是我的viewmodel基类中的一个简短方法,我用它来实例化和处理我的服务代理:



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





现在,我的大部分服务都是Async,而且我我有一个AsClient的异步变体,也可以正常工作:



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





每当我想要异步登录时,麻烦就开始了。显然,我不希望UI在我登录时冻结(或者访问任何WCF服务)。这本身工作正常,但问题出在我设置CurrentPrincipal的代码段中。你们大多数人可能都很熟悉这个问题:它似乎设置得很好。然后在我的程序中,我想使用CurrentPrincipal(在客户端或者将用户登录到messageheader中的WCF服务),但它似乎被重置为标准的GenericPrincipal。当我将登录恢复为同步时,CurrentPrincipal就可以了。所以简而言之:如何在异步代码中设置主体,让它在以后保持,而不是恢复到标准主体?

解决方案

这是一个非常有趣的问题。 :)



我首先想到的是 Thread.CurrentPrincipal 取决于当前线程和 async 方法最终会在错误的线程上设置它。但是,该属性似乎与逻辑调用上下文相关联,逻辑调用上下文正确地在 async 方法中的线程中流动。



但是,当方法完成时,似乎对 async 方法中的逻辑调用上下文所做的任何更改都将被丢弃。这仅适用于.NET 4.5;任何针对.NET 4.0并使用 Microsoft.Bcl.Async 库的代码将表现出这种行为!



Stephen Cleary StackOverflow上的答案 [ ^ ]讨论了与ASP.NET有关的这个问题,以及博客文章 [ ^ ]与问题有关:



在.NET 4.5。中,异步方法与逻辑调用上下文交互,因此它将更多使用异步方法正确流动。 ...在.NET 4.5中,在每个异步方法的开头,它激活其逻辑调用上下文的写时复制行为。当(如果)逻辑调用上下文被修改时,它将首先创建自己的本地副本。





不幸的是,似乎没有任何方法可以改变这种行为。只要 async 方法返回,对 CurrentPrincipal 属性的任何更改都将丢失。



可能能够使用 AppDomain.CurrentDomain.SetThreadPrincipal 方法 [ ^ ]。但是,此方法只能在 AppDomain 的生命周期内调用一次。如果您的应用程序允许用户退出然后再次登录,则需要使用自定义 IPrincipal 类作为真实 IPrincipal :

  public  密封  CurrentPrincipalFacade:IPrincipal 
{
private static readonly CurrentPrincipalFacade _instance = new CurrentPrincipalFacade();

private CurrentPrincipalFacade()
{
}

public static CurrentPrincipalFacade实例
{
get { return _instance; }
}

public IPrincipal WrappedValue { get ; set ; }

public IIdentity Identity
{
get {返回 WrappedValue == null null :WrappedValue.Identity; }
}

public bool IsInRole(字符串角色)
{
return WrappedValue!= null && WrappedValue.IsInRole(作用);
}
}

// 启动代码:
static void Main()
{
AppDomain.CurrentDomain .SetThreadPrincipal(CurrentPrincipalFacade.Instance);
...
}

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


Richard , 谢谢您的回复。好吧,至少我很高兴这不仅仅是我;)我进行了广泛的搜索,虽然我找到了一些ASP.NET解决方案(特别是Stephen Cleary提出的解决方案),但我并没有真正找到满足WPF的东西。



最初,我也沿着AppDomain路线走了,但正如你自己说的那样,我的应用程序必须具备登录和登录的能力,所以基本上是这个问题是一个简单的解决方案。



我决定尝试(它有效,虽然使用Thread.CurrentPrincipal的'正确'方式会很好,毕竟它是为它做的),如下:我有一个ObjectBase类,我用作所有客户端实体的基类。在那个类中,我已经有了一个公共静态CompositionContainer对象,在那里我存储了通过MEF进行依赖注入所需的所有目录。我刚刚包含了一个公共静态ClaimsPrincipal对象,当我登录时,我存储了我的身份验证调用返回的结果ClaimsPrincipal。登出后,我只需清除该对象。似乎工作得很好,正如你所期望的那样。



所以我的登录现在看起来像这样:



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





审核后,我可能会快速更改我的代码,并使用您的解决方案,因为它将问题分开好多了。再次,谢谢你!



为了完整性:我从代码中遗漏了一些不相关的东西,比如加密密码和所有这些。



所以,总而言之,问题是固定的,但我仍然希望将来的版本可以解决这个问题。关于.NET 4.6在这种情况下如何表现的任何想法?


如果您希望线程在新Windows中持久存在,您可以像这样设置线程。我看到有一个答案包含更多的自定义代码,当现实对象已经存在时



string [] =角色列表{a,b, c}

name=数据库中包含唯一标识的主键



AppDomain.CurrentDomain.SetThreadPrincipal(new GenericPrincipal(新的GenericIdentity(name,WindowsIdentity.GetCurrent()。AuthenticationType),string []);



我希望这可以帮助别人并感谢您阅读。

I've been searching the web for this, and couldn't really find a solution that actually worked. Situation is as follows: I've got a WPF application, where I want to present the user with a simple logon form. Trying to work MVVM, so I've got a LoginViewModel with the following code behind the login command:

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



"WithClient" is a short method in my viewmodel baseclass, which I use to instantiate and dispose of my service proxies:

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



Now, most of my services are Async, and I've got an async variant of WithClient going on, which also works fine:

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?

解决方案

This is a very interesting problem. :)

My first thought was that the Thread.CurrentPrincipal depends on the current thread, and an async method would end up setting it on the wrong thread. However, it appears that this property is associated with the logical call context, which correctly flows across threads in an async method.

However, it seems that any changes you make to the logical call context within an async method are discarded when the method completes. This only applies to .NET 4.5; any code targetting .NET 4.0 and using the Microsoft.Bcl.Async library will not exhibit this behaviour!

Stephen Cleary has an answer on StackOverflow[^] which discusses this problem in relation to ASP.NET, and a blog post[^] which is related to the problem:


In .NET 4.5., async methods interact with the logical call context so that it will more properly flow with async methods. ... In .NET 4.5, at the beginning of every async method, it activates a "copy-on-write" behavior for its logical call context. When (if) the logical call context is modified, it will create a local copy of itself first.



Unfortunately, there doesn't seem to be any way to change this behaviour. As soon as an async method returns, any changes to the CurrentPrincipal property will be lost.

You might be able to work around the problem by using the AppDomain.CurrentDomain.SetThreadPrincipal method[^]. However, this method can only be called once during the lifetime of an AppDomain. If your application allows users to sign out and then sign in again, you'll need to use a custom IPrincipal class to act as a façade to the real 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);
});


Richard, thank you for your reply. Well, at least I'm glad that it's not just me ;) I searched extensively, and although I found some solutions for ASP.NET (specifically the one Stephen Cleary proposed), I didn't really find anything satisfying for WPF.

Initially, I went down the AppDomain route as well, but as you said yourself, my application must indeed have to capability of login off and in, so that's basically out of the question as an easy solution.

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.

So my login just looks like this now:

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



After review, I will probably change my code real quick, and use your solution, as it separates out the issue much better. Again, thanks for this!

Just for completeness: I left out some non-relevant stuff from the code, such as the encryption for the password and all of that.

So, all in all, the problem is fixed, but I would still hope that a future release would remedy this. Any ideas on how .NET 4.6 behaves in this scenario?


If you want the thread to persist across new Windows you can set the thread like so. I saw there was an answer that contained more custom code when the reality is objects already exists

string[] = List of Roles {"a", "b", "c"}
"name" = the primary key from the database that holds the unique identity

AppDomain.CurrentDomain.SetThreadPrincipal(new GenericPrincipal(new GenericIdentity("name", WindowsIdentity.GetCurrent().AuthenticationType), string[]);

I hope this helps someone and thank you for reading.


这篇关于在WPF中的异步登录中设置CurrentPrincipal的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆