MachineKeyDataProtector - 通过后台作业发送确认电子邮件时无效 link

MachineKeyDataProtector - Invalid link when confirmation email sent through background job

我为此焦头烂额。每当通过我的 windows 服务(后台任务)发送用户注册电子邮件时,我都会收到 "Invalid link"。

我的设置

我在我们的开发服务器上使用 Hangfire 作为 windows 服务。这是有问题的 GenerateEmailConfirmationToken 调用发生的地方。它处于完全不同的上下文中,在 ASP.NET 管道之外。所以我设置了 machineKey 值以与 MVC 应用程序的 web.config 中的值相对应:

在转换为 MyApp.exe.config 的 Windows 服务控制台项目的 app.config 中,我有一个 machineKey 元素

在 MVC 5 项目中 - 我有一个匹配 MyApp.exe.config machineKey 元素的 machineKey 元素。

我已验证这两者具有相同的机器关键元素数据。

问题

当我使用 ASP.NET MVC 上下文和管道(IE 不经过 Hangfire 后台作业处理)生成用户时,link 工作正常.

当我使用后台作业处理器时,我总是无效link。我在这里完全没有想法。

为什么会这样?是因为令牌是在不同的线程中生成的吗?我该如何解决这个问题?

各个项目的相关代码

IoC 引导

被两个应用程序(Windows 服务和 MVC Web 应用程序)调用

container.Register<IUserTokenProvider<AppUser, int>>(() => DataProtector.TokenProvider, defaultAppLifeStyle);

DataProtector.cs

public class DataProtector
    {
        public static IDataProtectionProvider DataProtectionProvider { get; set; }
        public static DataProtectorTokenProvider<AppUser, int> TokenProvider { get; set; } 

        static DataProtector()
        {
            DataProtectionProvider = new MachineKeyProtectionProvider();
            TokenProvider = new DataProtectorTokenProvider<AppUser, int>(DataProtectionProvider.Create("Confirmation", "ResetPassword"));
        }
    }

我尝试过的事情

使用 DpapiDataProtectionProvider

来自 Generating reset password token does not work in Azure Website

的自定义 MachineKeyProtectionProvider

MachineKeyProtectionProvider.cs 代码与上面 linked post 完全相同。

我也尝试过 "YourMom" 和 "AllYourTokensAreBelongToMe" 等其他目的,但都无济于事。单一用途,多重用途 - 没关系 - none 工作。

我也在调用 HttpUtility.UrlEncode(code) 在两个地方(控制器和后台作业)生成的代码。

解决方案

igor 做对了,只是这不是代码问题。这是因为一个流氓服务接管了这份工作,它有一个不同的机器密钥。我一直盯着这个问题看太久了,没看到第二个服务 运行.

据我了解您的问题,有 2 个地方可能会发生故障。


1。机器密钥

可能是 MachineKey 本身没有在您的 2 个应用程序之间产生一致的值。如果 .config 文件中的 machineKey 在两个应用程序中不相同,就会发生这种情况(我读到你检查过它但是一个简单的 type-o,添加 space, 添加到错误的父元素等可能会导致此行为。)。这可以很容易地测试以将其排除为故障点。此外,根据引用的 .net 框架,行为可能会有所不同,MachineKey.Protect

The configuration settings that are required for the MachineKeyCompatibilityMode.Framework45 option are required for this method even if the MachineKeySection.CompatibilityMode property is not set to the Framework45 option.

我创建了一个随机密钥对用于测试,并使用这个密钥生成了一个测试值,我在下面的代码中分配给了变量 validValue。如果您 copy/paste 将以下部分放入 web.config 和 app.config,则该键值的 Unprotect 将起作用。

web.config / app.config

<system.web>
  <httpRuntime targetFramework="4.6.1"/>
    <machineKey decryption="AES" decryptionKey="9ADCFD68D2089D79A941F9B8D06170E4F6C96E9CE996449C931F7976EF3DD209"  validation="HMACSHA256" validationKey="98D92CC1E5688DB544A1A5EF98474F3758C6819A93CC97E8684FFC7ED163C445852628E36465DB4E93BB1F8E12D69D0A99ED55639938B259D0216BD2DF4F9E73" />
</system.web>

服务应用测试

class Program
{
    static void Main(string[] args)
    {
        // should evaluate to SomeTestString
        const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
        var unprotected2 = MachineWrapper.Unprotect(validValue);
    }
}

Mvc 控制器(或 Web Api 控制器)测试

public class WebTestController : Controller
{
    // GET: WebTest
    public ActionResult Index()
    {
        // should evaluate to SomeTestString
        const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
        var unprotected2 = MachineWrapper.Unprotect(validValue);

        return View(unprotected2);
    }
}

通用代码

using System;
using System.Linq;
using System.Text;
using System.Web.Security;

namespace Common
{
    public class MachineWrapper
    {
        public static string Protect()
        {
            var testData = "SomeTestString";
            return BytesToString(MachineKey.Protect(System.Text.Encoding.UTF8.GetBytes(testData), "PasswordSafe"));
        }

        public static string Unprotect(string data)
        {
            var bytes = StringToBytes(data);
            var result = MachineKey.Unprotect(bytes, "PasswordSafe");
            return System.Text.Encoding.UTF8.GetString(result);
        }

        public static byte[] StringToBytes(string hex)
        {
            return Enumerable.Range(0, hex.Length)
                .Where(x => x % 2 == 0)
                .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                .ToArray();
        }
        public static string BytesToString(byte[] bytes)
        {
            var hex = new StringBuilder(bytes.Length * 2);
            foreach (byte b in bytes)
                hex.AppendFormat("{0:x2}", b);
            return hex.ToString().ToUpper();
        }
    }
}

如果此操作通过,控制台和 Web 应用程序将获得相同的值并且不会抛出 CryptographicException 消息 Error occurred during a cryptographic operation。如果您想使用自己的密钥进行测试,只需 运行 保护通用 MachineWrapper class 并记录值并为两个应用程序重新执行。


2。 UserManager 使用了错误的类型

我将从上一节开始,但另一个失败点是 Microsoft.AspNet.Identity.UserManager 未使用您的自定义机器密钥提供程序。所以这里有一些 questions/action 项目可以帮助您弄清楚为什么会发生这种情况:

  1. container.Register 是 Unity IoC 框架还是您正在使用其他框架?
  2. 您确定您的 Di 框架也在 服务应用程序和 Web 应用程序的 Microsoft.AspNet.Identity.UserManager 中注入了该实例吗?
  3. 已在 MachineKeyDataProtector class 的 public byte[] Protect 中放置一个断点,以查看是否在 both 服务应用程序中调用了它以及 Web 应用程序?

从目前我看到的示例(包括您使用自定义 MachineKey 解决方案发布的示例)中,您需要在应用程序启动期间手动 bootstrap 类型,但我又一次没有尝试连接到使用 DI 替换此组件的标识框架。

如果您查看创建新 MVC 应用程序时提供的默认 Visual Studio 模板代码,代码文件 App_Start\IdentityConfig.cs 将是添加此新提供程序的位置。

方法:

public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)

替换

var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
    manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}

有了这个

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

并且必须为两个应用程序配置它,如果你没有使用配置它的公共库。