MachineKeyDataProtector-通过后台作业发送确认电子邮件时的无效链接 [英] MachineKeyDataProtector - Invalid link when confirmation email sent through background job

查看:92
本文介绍了MachineKeyDataProtector-通过后台作业发送确认电子邮件时的无效链接的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在为此扯头发.每当通过我的Windows服务发送用户注册电子邮件时(后台任务),我都会收到无效链接".

I've been pulling my hair out over this. Anytime a user registration email is sent out via my windows service (background task), I get an "Invalid link".

我的设置

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

I'm using Hangfire as a windows service on our development server. This is where the problematic GenerateEmailConfirmationToken call is happening. It's in a completely different context, outside of the ASP.NET pipeline. So I have setup machineKey values to correspond with that in the web.config of the MVC application:

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

In the app.config of the Windows Service Console project, which transforms to MyApp.exe.config, I have a machineKey element

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

In the MVC 5 project - I have a machineKey element that matches the MyApp.exe.config machineKey element.

我已经验证了两者都具有相同的机器关键元素数据.

I've verified that BOTH of these have the same machine key element data.

问题

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

When I generate a user using the ASP.NET MVC context and pipeline (IE without going through the Hangfire Background job processing), the link works fine.

当我使用后台作业处理器时,我总是得到无效的链接.我在这里没主意.

When I use the background job processor, I always get invalid link. I'm all out of ideas here.

为什么会这样?是因为令牌是在其他线程中生成的吗?我该如何解决?

Why is this happening? Is it because the token is being generated in a different thread? How do I get around this?

各个项目的相关代码

IoC引导

被两个应用程序(Windows Service和MVC Web App)调用

Gets called by both applications (Windows Service and MVC Web App)

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

自定义 MachineKeyProtectionProvider 23661872#23661872>在Azure网站中无法生成重置密码令牌

Custom MachineKeyProtectionProvider from Generating reset password token does not work in Azure Website

MachineKeyProtectionProvider.cs 代码与上面的链接文章完全相同.

The MachineKeyProtectionProvider.cs code is exactly as the linked post above.

我还尝试了其他目的,例如"YourMom"和"AllYourTokensAreBelongToMe",都无济于事.单一目的,多重目的-没关系-没有用.

I've also tried other purposes like "YourMom" and "AllYourTokensAreBelongToMe" to no avail. Single purposes, multiple purposes - it doesn't matter - none work.

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

I'm also calling HttpUtility.UrlEncode(code) on the code that gets generated in both places (Controller and Background Job).

解决方案

igor正确无误,只是它不是代码问题.这是因为流氓服务人员接管了该工作,而该机器具有不同的机器密钥.很久以来我一直在盯着这个问题,以至于我看不到第二个服务正在运行.

igor got it right, except it was not a code issue. It was because of a rogue service picking up the job, which had a different machine key. I had been staring at the problem so long that I did not see a second service running.

推荐答案

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

As I understand your problem there are 2 possible places where failure could occur.

可能是 MachineKey 本身在两个应用程序之间未产生一致的值.如果您在两个应用程序中的 .config 文件中的 machineKey 不相同(我确实读过您检查过它,但只是一个简单的type-o,添加空间,添加到错误的父元素等可能导致此行为.).可以很容易地对其进行测试,以将其排除在故障点之外.而且,行为可能会有所不同,具体取决于所引用的.net框架

It could be that the MachineKey itself is not producing a consistent value between your 2 applications. This can happen if your machineKey in the .config file is not the same in both applications (I did read that you checked it but a simple type-o, added space, added to the wrong parent element, etc. could lead to this behavior.). This can be easily tested to rule it out as a point of failure. Also the behavior might be different depending on the referenced .net framework, MachineKey.Protect

即使MachineKeySection.CompatibilityMode属性未设置为Framework45选项,此方法也需要MachineKeyCompatibilityMode.Framework45选项所需的配置设置.

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 .如果将以下部分复制/粘贴到web.config和app.config中,则该键值的 Unprotect 将起作用.

I created a random key pair for testing and using this key I generated a test value I assigned to variable validValue below in the code. If you copy/paste the following section into your web.config and app.config the Unprotect of that keyvalue will work.

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控制器)测试

Mvc Controller (or Web Api controller) Test

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 消息在加密操作期间发生错误.如果您想使用自己的密钥进行测试,只需在常见的 MachineWrapper 类中运行Protect,然后记录该值并为两个应用程序重新执行即可.

If this passes both Console and the Web Application will get the same value and not throw a CryptographicException message Error occurred during a cryptographic operation. If you want to test with your own keys just run Protect from the common MachineWrapper class and record the value and re-execute for both apps.

我将从上一节开始,但另一个失败点是 Microsoft.AspNet.Identity.UserManager 未使用您的自定义计算机密钥提供程序.因此,这里有一些问题/措施可以帮助您弄清为什么会发生这种情况:

I would start with the previous section BUT the other failure point is that your custom machine key provider is not being used by the Microsoft.AspNet.Identity.UserManager. So here are some questions/action items that can help you figure out why this is happening:

  1. container.Register 是Unity IoC框架还是您正在使用其他框架?
  2. 您确定您的Di框架也在将两者服务应用程序以及Web应用程序中的 Microsoft.AspNet.Identity.UserManager 中注入了该实例吗?
  3. 已在您的 MachineKeyDataProtector 类的 public byte [] Protect 中设置了一个断点,以查看是否在服务应用程序的两者中都调用了该断点以及Web应用程序?
  1. Is container.Register the Unity IoC framework or are you using another framework?
  2. Are you sure that your Di framework is also injecting that instance in the Microsoft.AspNet.Identity.UserManager in both the Service application as well as the Web application?
  3. Have put a break point in public byte[] Protect of your MachineKeyDataProtector class to see if this is called in both the Service application as well as the Web application?

从到目前为止我看到的示例(包括您使用自定义MachineKey解决方案发布的示例)中,您需要在应用程序启动期间手动引导类型,但是我再也没有尝试使用Identity框架来替换该组件使用DI.

From examples I have seen so far (including the one you posted with the custom MachineKey solution) you need to manually bootstrap the type during application startup but then again I have not ever tried to hook into the Identity framework to replace this component using DI.

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

If you look at the default Visual Studio template code that is provided when you create a new MVC application the code file App_Start\IdentityConfig.cs would be the place to add this new provider.

方法:

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"));

如果您没有使用配置了该库的公用库,则必须为两个应用程序进行配置.

And this has to be configured for both applications if you are not using a common library where this is configured.

这篇关于MachineKeyDataProtector-通过后台作业发送确认电子邮件时的无效链接的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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