在Azure网站中生成重置密码令牌无法正常工作

60

我正在使用内置的 ASP.NET 5 UserManager 类来实现网站上的重置密码功能。

在我的开发环境中一切正常。然而,一旦我尝试在作为 Azure 网站运行的生产站点上使用它,就会出现以下异常:

System.Security.Cryptography.CryptographicException: 数据保护操作失败。这可能是因为当前线程的用户上下文未加载用户配置文件所导致的,当线程进行模拟时可能会出现这种情况。

这是我设置 UserManager 实例的方式:

var provider = new Microsoft.Owin.Security.DataProtection.DpapiDataProtectionProvider(SiteConfig.SiteName);
UserManager.UserTokenProvider = new Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider<User>(provider.Create(ResetPasswordPurpose));

然后,我这样生成令牌(将其发送到用户的电子邮件中,以便他们可以验证确实希望重置密码):

string token = UserManager.GeneratePasswordResetToken(user.Id);

很不幸,当在Azure上运行时,我遇到了上面的异常。

我在Google上搜索到了这个可能的解决方案。然而,它完全没有起作用,我仍然得到相同的异常。

根据该链接,这与会话令牌在像Azure这样的Web farm上无法工作有关。


请投票支持此问题 https://aspnetidentity.codeplex.com/workitem/2439 以解决此问题。 - trailmax
在 Azure VM 上运行时,我遇到了相同的错误。这是由于同样的原因引起的吗?还是应该提一个新的 SO 问题? - Richard Garside
4个回答

92

DpapiDataProtectionProvider使用DPAPI,但在Web farm/Cloud环境中无法正常工作,因为只有加密数据的机器才能解密它。您需要一种加密数据的方式,使其可以在环境中的任何机器上解密。不幸的是,ASP.NET Identity 2.0除了DpapiDataProtectionProvider之外没有其他IProtectionProvider的实现。但是,自己编写也并不难。

其中一个选择是使用MachineKey类,如下所示:

public class MachineKeyProtectionProvider : IDataProtectionProvider
{
    public IDataProtector Create(params string[] purposes)
    {
        return new MachineKeyDataProtector(purposes);
    }
}

public class MachineKeyDataProtector : IDataProtector
{
    private readonly string[] _purposes;

    public MachineKeyDataProtector(string[] purposes)
    {
        _purposes = purposes;
    }

    public byte[] Protect(byte[] userData)
    {
        return MachineKey.Protect(userData, _purposes);
    }

    public byte[] Unprotect(byte[] protectedData)
    {
        return MachineKey.Unprotect(protectedData, _purposes);
    }
}
为了使用这个选项,您需要遵循几个步骤。 第一步 修改代码以使用MachineKeyProtectionProvider。
using Microsoft.AspNet.Identity.Owin;
// ...

var provider = new MachineKeyProtectionProvider();
UserManager.UserTokenProvider = new DataProtectorTokenProvider<User>(
    provider.Create("ResetPasswordPurpose"));

步骤2

同步网农/云环境中所有机器的MachineKey值。听起来有点可怕,但这其实是我们以前为了在网农环境中正确工作而不断执行的与视图状态验证相同的步骤(还使用了DPAPI)。


在这里也有报道(http://tech.trailmax.info/2014/06/asp-net-identity-and-cryptographicexception-when-running-your-site-on-microsoft-azure-web-sites/#comment-1640502238)。这是对asp.net团队的[UserVoice](https://aspnet.uservoice.com/forums/41199-general-asp-net)和[Codeplex Issues](https://aspnetidentity.codeplex.com/WorkItem/Create?ProjectName=aspnetidentity)的很好的反馈。你会提出问题吗? - OzBob
@yqit 你可以把它放在任何地方,但是它需要在应用程序启动时执行。例如,在MVC 5应用程序中,它应该由MvcApplication.Application_Start()执行。 - Chris Staley
我已经使用这个工具很长时间并取得成功,但现在我们添加了一个.NET Core项目。我现在需要如何做呢?考虑到这份文档:https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/compatibility/replacing-machinekey 以及这份关于在Azure Blob存储中存储的文档:https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-providers - CularBytes
@CularBytes 首先请注意,我的答案已经有近4年的历史了,因此自那时起API和底层平台发生了很多变化。在这一点上,其他得到赞同的答案可能更有帮助。话虽如此,看起来你情况下唯一的变化是第二步,因为你想将机器密钥存储在Azure Blob Storage中。文档中有一个示例,指出了如何实现这一点:https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-providers#azure-and-redis - Chris Staley
你真的救了我的一天,非常感谢!我不需要步骤2,我成功地在 Plesk Obsidian 18 上托管了一个 WIF 应用程序。 - MAXE
显示剩余2条评论

47
考虑使用 IAppBuilder.GetDataProtectionProvider() 替代声明新的 DpapiDataProtectionProvider
与您类似,我也是通过配置我的 UserManager 来引入这个问题的,代码示例如下:
public class UserManager : UserManager<ApplicationUser>
{
    public UserManager() : base(new UserStore<ApplicationUser>(new MyDbContext()))
    {
        // this does not work on azure!!!
        var provider = new Microsoft.Owin.Security.DataProtection.DpapiDataProtectionProvider("ASP.NET IDENTITY");
        this.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(provider.Create("EmailConfirmation"))
        {
            TokenLifespan = TimeSpan.FromHours(24),
        };
    }
}

上面链接到的 CodePlex 问题实际上是引用了一个博客文章,该文章已经更新了一个更简单的解决方案。该建议是保存对 IDataProtector 的静态引用...

public partial class Startup
{
    internal static IDataProtectionProvider DataProtectionProvider { get; private set; }

    public void ConfigureAuth(IAppBuilder app)
    {
        DataProtectionProvider = app.GetDataProtectionProvider();
        // other stuff.
    }
}

...然后在UserManager内引用它

public class UserManager : UserManager<ApplicationUser>
{
    public UserManager() : base(new UserStore<ApplicationUser>(new MyDbContext()))
    {
        var dataProtectionProvider = Startup.DataProtectionProvider;
        this.UserTokenProvider = 
                new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));

        // do other configuration
    }
}

johnso的回答还提供了一个很好的例子,展示了如何使用Autofac来连接它。


这是否解决了顶部答案中提到的DPAPI在Web Farm中无法工作的问题? - BenCr
不是的,但它允许ASP.NET Identity 2.0在Azure网站(Web应用程序)中工作,我认为这就是问题的意图。顶部答案在Azure上有自己的问题,因为设置MachineKey会带来自己的挑战:https://dev59.com/z10b5IYBdhLWcg3wIN99#29765371 - Loren Paulsen
感谢@Loren,即使您有自定义的Identity实现,它也可以很好地工作。 - dalcam
这对我有用。非常感谢@LorenPaulsen,你救了我的一天。 - Syed Ali Taqi
在Azure上,Webfarms或VMs(其中您可以设置/同步机器密钥)与Azure应用服务(Azure网站)之间确实存在差异,您无法设置机器密钥。这个答案将解决后者。另一个答案解决前者。 - Peter

25

我遇到了同样的问题,但我是在Amazon EC2上托管。
我通过进入IIS中的应用程序池并(右键单击后在高级设置下)将进程模型-加载用户配置文件=true来解决它。


我也用过这个方法,很好用! - Stoyan Berov
这对我有用,谢谢!我只是在 Account 控制器之外生成自定义令牌。 - Watson Kaunda
1
你是一个英雄。非常感谢你,你救了我的一天。 - Yargicx

12

我遇到了同样的问题(当在Azure上运行时Owin.Security.DataProtection.DpapiDataProtectionProvider会失败),而Staley是正确的,你不能使用DpapiDataProtectionProvider

如果你正在使用OWIN启动类,你可以避免自己编写IDataProtectionProvider,而是使用IAppBuilderGetDataProtectionProvider方法。

例如,使用Autofac:

internal static IDataProtectionProvider DataProtectionProvider;    

public void ConfigureAuth(IAppBuilder app)
{
    // ...

    DataProtectionProvider = app.GetDataProtectionProvider();

    builder.Register<IDataProtectionProvider>(c => DataProtectionProvider)
        .InstancePerLifetimeScope();

    // ...
}

感谢您尽管有人踩了一下,仍然保留了这个答案。它对我非常有效。 - Rob Lyndon
“builder.RegisterInstance(app.GetDataProtectionProvider());” 不是更好的选择吗? - Marc L.
1
将WEBSITE_LOAD_USER_PROFILE添加到web.config中无效,在Azure中添加设置非常好,但是使用app.GetDataProtectionProvider()请求DataProtectionProvider是我找到的最佳解决方案。谢谢! - Vincent

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接