MachineKeyDataProtector-通过后台作业发送确认电子邮件时的无效链接 [英] MachineKeyDataProtector - Invalid link when confirmation email sent through background job
问题描述
我一直在为此扯头发.每当通过我的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:
-
container.Register
是Unity IoC框架还是您正在使用其他框架? - 您确定您的Di框架也在将两者服务应用程序以及Web应用程序中的
Microsoft.AspNet.Identity.UserManager
中注入了该实例吗? - 已在您的
MachineKeyDataProtector
类的public byte [] Protect
中设置了一个断点,以查看是否在服务应用程序的两者中都调用了该断点以及Web应用程序?
- Is
container.Register
the Unity IoC framework or are you using another framework? - 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? - 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?
从到目前为止我看到的示例(包括您使用自定义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屋!