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".
我把头发都扯下来了。当用户注册邮件通过我的windows服务(后台任务)发送出去时,我得到一个“无效链接”。
My setup
我的设置
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:
我在开发服务器上使用Hangfire作为windows服务。这就是出现问题的GenerateEmailConfirmationToken调用的地方。它处于一个完全不同的上下文中,在ASP之外。净管道。所以我设置了machineKey值来对应web上的值。MVC应用的配置:
In the app.config of the Windows Service Console project, which transforms to MyApp.exe.config, I have a machineKey element
在Windows服务控制台项目的app.config中,该项目将转换为MyApp.exe。配置,我有一个机器关键元素。
In the MVC 5 project - I have a machineKey element that matches the MyApp.exe.config machineKey element.
在MVC 5项目中——我有一个与MyApp.exe匹配的machineKey元素。配置machineKey元素。
I've verified that BOTH of these have the same machine key element data.
我已经验证了这两个都有相同的machine key element数据。
The Problem
这个问题
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.
当我使用ASP生成用户时。NET MVC上下文和管道(即不需要经过Hangfire后台作业处理),链接工作正常。
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?
为什么会这样?是因为该令牌是在另一个线程中生成的吗?我该怎么解决这个问题呢?
Relevant code for the various projects
各种项目的相关代码
IoC Bootstrapping
国际奥委会引导
Gets called by both applications (Windows Service and MVC Web App)
被两个应用程序调用(Windows服务和MVC Web应用程序)
container.Register<IUserTokenProvider<AppUser, int>>(() => DataProtector.TokenProvider, defaultAppLifeStyle);
DataProtector.cs
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"));
}
}
Things I've Tried
我已经试过
Using a DpapiDataProtectionProvider
使用DpapiDataProtectionProvider
Custom MachineKeyProtectionProvider
from Generating reset password token does not work in Azure Website
自定义MachineKeyProtectionProvider从生成重置密码令牌并不在Azure网站中工作
The MachineKeyProtectionProvider.cs
code is exactly as the linked post above.
MachineKeyProtectionProvider。cs代码与上面链接的文章完全一样。
I've also tried other purposes like "YourMom" and "AllYourTokensAreBelongToMe" to no avail. Single purposes, multiple purposes - it doesn't matter - none work.
我还尝试了其他的一些目的,比如“你妈妈”和“你自己的归属”。单目的,多目的——没关系——都没用。
I'm also calling HttpUtility.UrlEncode(code)
on the code that gets generated in both places (Controller and Background Job).
我还把HttpUtility.UrlEncode(代码)调用在两个位置(控制器和后台作业)生成的代码中。
Solution
解决方案
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.
igor说对了,除了它不是代码问题。这是因为一个流氓服务接手了这个任务,它有一个不同的机器密钥。我盯着这个问题看了这么久,以至于没有看到第二个服务在运行。
1 个解决方案
#1
1
As I understand your problem there are 2 possible places where failure could occur.
正如我理解你的问题,有两个地方可能会发生失败。
1. MachineKey
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
可能是机器键本身并没有在您的两个应用程序之间产生一致的值。如果在.config文件中的machineKey在两个应用程序中都不相同(我确实读过您检查过它,但是一个简单的类型-o、添加的空格、添加到错误的父元素等可能导致这种行为),这可能会发生。可以很容易地测试它,将其排除为故障点。另外,根据所引用的。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.
机器键盘兼容模式所需的配置设置。这个方法需要Framework45选项,即使是MachineKeySection。相容模式属性没有设置为Framework45选项。
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.
我创建了一个用于测试的随机密钥对,并使用这个密钥生成了一个测试值,我将它分配给代码下面的变量validValue。如果您将以下部分复制/粘贴到您的web中。config和app.config对该键值的不保护将起作用。
web.config / app.config
网络。配置/ app.config
<system.web>
<httpRuntime targetFramework="4.6.1"/>
<machineKey decryption="AES" decryptionKey="9ADCFD68D2089D79A941F9B8D06170E4F6C96E9CE996449C931F7976EF3DD209" validation="HMACSHA256" validationKey="98D92CC1E5688DB544A1A5EF98474F3758C6819A93CC97E8684FFC7ED163C445852628E36465DB4E93BB1F8E12D69D0A99ED55639938B259D0216BD2DF4F9E73" />
</system.web>
Service Application Test
服务应用程序测试
class Program
{
static void Main(string[] args)
{
// should evaluate to SomeTestString
const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
var unprotected2 = MachineWrapper.Unprotect(validValue);
}
}
Mvc Controller (or Web Api controller) Test
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);
}
}
Common Code
通用代码
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();
}
}
}
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.
如果这通过了控制台和Web应用程序,则会得到相同的值,而不会抛出加密操作期间发生的密码异常消息错误。如果您想使用自己的密钥进行测试,只需运行普通的MachineWrapper类的Protect,并记录下两个应用程序的值并重新执行。
2. UserManager uses Wrong Type
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:
我将从上一节开始,但是另一个失败点是您的自定义机器密钥提供程序没有被Microsoft.AspNet.Identity.UserManager使用。这里有一些问题/行动项目可以帮助你理解为什么会这样:
- Is
container.Register
the Unity IoC framework or are you using another framework? - 是容器。注册Unity IoC框架还是使用其他框架?
- 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? - 确定您的Di框架也将该实例注入到Microsoft.AspNet.Identity中。服务应用程序和Web应用程序中的UserManager ?
- Have put a break point in
public byte[] Protect
of yourMachineKeyDataProtector
class to see if this is called in both the Service application as well as the Web application? - 是否在您的machinekeydataprotection类的公共字节[]中设置了断点,以查看在服务应用程序和Web应用程序中是否都调用了它?
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.
从我到目前为止看到的示例(包括您使用定制的MachineKey解决方案发布的示例)中,您需要在应用程序启动期间手动引导类型,但同样地,我从未尝试过使用DI连接到标识框架来替换此组件。
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.
如果您查看默认的Visual Studio模板代码,当您创建一个新的MVC应用程序时,代码文件App_Start\IdentityConfig。cs将是添加这个新的提供者的地方。
Method:
方法:
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
Replace
取代
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
With this
用这个
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.
如果您不使用公共库(配置公共库),这两个应用程序都必须进行配置。
#1
1
As I understand your problem there are 2 possible places where failure could occur.
正如我理解你的问题,有两个地方可能会发生失败。
1. MachineKey
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
可能是机器键本身并没有在您的两个应用程序之间产生一致的值。如果在.config文件中的machineKey在两个应用程序中都不相同(我确实读过您检查过它,但是一个简单的类型-o、添加的空格、添加到错误的父元素等可能导致这种行为),这可能会发生。可以很容易地测试它,将其排除为故障点。另外,根据所引用的。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.
机器键盘兼容模式所需的配置设置。这个方法需要Framework45选项,即使是MachineKeySection。相容模式属性没有设置为Framework45选项。
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.
我创建了一个用于测试的随机密钥对,并使用这个密钥生成了一个测试值,我将它分配给代码下面的变量validValue。如果您将以下部分复制/粘贴到您的web中。config和app.config对该键值的不保护将起作用。
web.config / app.config
网络。配置/ app.config
<system.web>
<httpRuntime targetFramework="4.6.1"/>
<machineKey decryption="AES" decryptionKey="9ADCFD68D2089D79A941F9B8D06170E4F6C96E9CE996449C931F7976EF3DD209" validation="HMACSHA256" validationKey="98D92CC1E5688DB544A1A5EF98474F3758C6819A93CC97E8684FFC7ED163C445852628E36465DB4E93BB1F8E12D69D0A99ED55639938B259D0216BD2DF4F9E73" />
</system.web>
Service Application Test
服务应用程序测试
class Program
{
static void Main(string[] args)
{
// should evaluate to SomeTestString
const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
var unprotected2 = MachineWrapper.Unprotect(validValue);
}
}
Mvc Controller (or Web Api controller) Test
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);
}
}
Common Code
通用代码
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();
}
}
}
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.
如果这通过了控制台和Web应用程序,则会得到相同的值,而不会抛出加密操作期间发生的密码异常消息错误。如果您想使用自己的密钥进行测试,只需运行普通的MachineWrapper类的Protect,并记录下两个应用程序的值并重新执行。
2. UserManager uses Wrong Type
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:
我将从上一节开始,但是另一个失败点是您的自定义机器密钥提供程序没有被Microsoft.AspNet.Identity.UserManager使用。这里有一些问题/行动项目可以帮助你理解为什么会这样:
- Is
container.Register
the Unity IoC framework or are you using another framework? - 是容器。注册Unity IoC框架还是使用其他框架?
- 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? - 确定您的Di框架也将该实例注入到Microsoft.AspNet.Identity中。服务应用程序和Web应用程序中的UserManager ?
- Have put a break point in
public byte[] Protect
of yourMachineKeyDataProtector
class to see if this is called in both the Service application as well as the Web application? - 是否在您的machinekeydataprotection类的公共字节[]中设置了断点,以查看在服务应用程序和Web应用程序中是否都调用了它?
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.
从我到目前为止看到的示例(包括您使用定制的MachineKey解决方案发布的示例)中,您需要在应用程序启动期间手动引导类型,但同样地,我从未尝试过使用DI连接到标识框架来替换此组件。
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.
如果您查看默认的Visual Studio模板代码,当您创建一个新的MVC应用程序时,代码文件App_Start\IdentityConfig。cs将是添加这个新的提供者的地方。
Method:
方法:
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
Replace
取代
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
With this
用这个
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.
如果您不使用公共库(配置公共库),这两个应用程序都必须进行配置。