解决因 DataProtection 配置导致的 ASP.NET Core 站点间认证 Cookie 共享失败问题
在现代微服务或分布式架构中,我们经常需要将单一应用拆分为多个独立的 ASP.NET Core 应用。一个常见的需求是,用户在一个应用(例如 www.app.com)登录后,访问另一个相关联的应用(例如 shop.app.com)时,应保持登录状态。这种单点登录(SSO)场景的核心技术就是共享认证 Cookie。
然而,许多开发者在尝试实现此功能时,会遇到一个令人困惑的问题:明明在两个应用中配置了相同的 Cookie 名称和域,但登录状态仍然无法共享。用户在站点 A 登录后,访问站点 B 时依然被重定向到登录页面。这十有八九是 ASP.NET Core Data Protection 系统在“作祟”。
本文将深入剖析 Data Protection 的工作原理,解释为何不正确的配置会导致 Cookie 共享失败,并提供详细的解决方案、最佳实践和代码示例。
目录#
- 问题背景:为什么 Cookie 需要被“保护”?
- 深入理解 ASP.NET Core Data Protection
- 问题根因:密钥环不一致
- 解决方案:配置共享的密钥环
- 配置统一的应用程序名称
- 完整示例:两个站点的协同配置
- 最佳实践与常见陷阱
- 总结
- 参考资料
问题背景:为什么 Cookie 需要被“保护”?#
ASP.NET Core 的认证 Cookie(如 AspNetCore.Cookies)默认不是明文存储的。为了安全起见,它包含了加密的认证票据(Authentication Ticket),其中含有用户的声明(Claims)、过期时间等敏感信息。
如果 Cookie 被恶意截获,攻击者可能会尝试篡改其中的内容(例如,将用户名改为 admin)或直接进行重放攻击。为了防止这种情况,ASP.NET Core 使用 Data Protection 系统对 Cookie 进行:
- 加密:确保 Cookie 内容不可读。
- 验证:确保 Cookie 内容未被篡改。
简单来说,Data Protection 是 Cookie 的“签名和加密器”。站点 A 加密的 Cookie,必须由同一个“签名和加密器”才能被站点 B 解密和验证。
深入理解 ASP.NET Core Data Protection#
Data Protection 系统的核心是一个密钥环。这个密钥环包含一个或多个用于加密和验证数据的密钥。
-
默认行为:当 ASP.NET Core 应用启动时,如果未显式配置 Data Protection,它会尝试根据当前运行环境自动创建一个密钥环。
- 在 Windows 上,它可能使用 DPAPI(当前用户范围)存储密钥。
- 在 macOS/Linux 上,它通常将密钥存储在
~/.aspnet/DataProtection-Keys目录下。
-
关键点:每个独立部署的 ASP.NET Core 应用默认都会生成自己独有的、彼此不知晓的密钥环。
这就导致了我们的核心问题。
问题根因:密钥环不一致#
想象一下,站点 A 和站点 B 就像两个不同的银行。
- 站点 A 用自己的印章(密钥环)给客户开具了一张加密的存单(认证 Cookie)。
- 当客户拿着这张存单去站点 B 取款时,站点 B 拿出自己的印章进行核对,发现印章不匹配。因此,站点 B 会断定这张存单是无效的或伪造的,于是拒绝服务(要求重新登录)。
这就是不同站点的 Data Protection 系统使用不同密钥环所导致的结果。即使 Cookie 的域名(.app.com)设置正确,其内容也无法被另一个站点解密和验证。
解决方案:配置共享的密钥环#
要让多个应用共享认证 Cookie,我们必须让它们共享同一个密钥环。这意味着我们需要将一个持久的、可被所有应用访问的存储位置配置给 Data Protection 系统。
方案一:使用共享的文件系统(适用于本地或同一服务器)#
对于部署在同一台物理机或虚拟机上的多个应用,这是最简单的方法。
在 Program.cs 中配置:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@”\\server\share\Directory–Containing–Keys–For–All–Apps“))
.SetApplicationName(“MySharedApp”); // 非常重要!见下文解释
// ... 其他服务配置,如 AddAuthentication说明:
PersistKeysToFileSystem:指定一个所有应用都有读写权限的共享网络路径或本地路径。SetApplicationName:确保所有应用使用相同的应用名称。这用于隔离不同应用组的密钥。必须一致。
方案二:使用 Redis(推荐用于分布式环境)#
在云原生或分布式环境中,Redis 是共享密钥环的理想选择,因为它高性能且为分布式而设计。
-
安装 NuGet 包:
Install-Package Microsoft.AspNetCore.DataProtection.StackExchangeRedis -
在
Program.cs中配置:var builder = WebApplication.CreateBuilder(args); // 配置 Redis 连接 var redisConnection = “your_redis_connection_string”; builder.Services.AddStackExchangeRedisCache(options => // 如果也用 Redis 做缓存,可选项 { options.Configuration = redisConnection; }); // 配置 Data Protection 使用 Redis builder.Services.AddDataProtection() .PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(redisConnection), “DataProtection-Keys”) // “DataProtection-Keys” 是 Redis 中存储密钥的 Key 名 .SetApplicationName(“MySharedApp”); // ... 其他配置
方案三:使用 Azure Blob Storage 或数据库#
你也可以使用其他持久化存储,例如 Azure Blob Storage(通过 Microsoft.AspNetCore.DataProtection.AzureStorage 包)或 EF Core(通过 Microsoft.AspNetCore.DataProtection.EntityFrameworkCore 包)。配置模式类似,都是调用相应的 PersistKeysTo[Store] 方法。
配置统一的应用程序名称#
SetApplicationName("MySharedApp") 这一步至关重要,但常常被忽略。
- 作用:Data Protection 使用应用程序名称来隔离不同应用的密钥。即使共享同一个物理存储(如同一个 Redis 实例),名为
AppA的应用也不会去使用名为AppB的应用创建的密钥。 - 要求:为了共享 Cookie,所有需要互信的应用必须设置完全相同的
ApplicationName。
// 在所有需要共享 Cookie 的应用中,使用相同的字符串
.SetApplicationName(“MySharedApp”);完整示例:两个站点的协同配置#
假设我们有两个应用:WebApp (www.app.com) 和 ShopApp (shop.app.com)。我们希望实现 SSO。
1. 配置认证方案(两个应用相同)
在 Program.cs 中,配置一个统一的认证 Cookie 方案。
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = “.AspNetCore.SharedCookie”; // Cookie 名称可以自定义,但要一致
options.Cookie.Domain = “.app.com”; // 设置父级域名,确保子域名可接收
options.Cookie.SameSite = SameSiteMode.Lax; // 或 None (如果使用 HTTPS)
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // 生产环境务必使用 HTTPS
options.LoginPath = “/Account/Login”;
// ... 其他配置
});2. 配置共享的 Data Protection(两个应用相同)
以 Redis 为例:
// 假设 Redis 连接字符串从配置中读取
var redisConn = builder.Configuration.GetConnectionString(“Redis”);
builder.Services.AddDataProtection()
.PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(redisConn), “DataProtection-Keys”)
.SetApplicationName(“ECommercePlatform”); // 两个应用都使用这个名称现在,当用户在 www.app.com 登录时,该站点会使用共享密钥环加密一个 Cookie 并写入 .app.com 域。当用户访问 shop.app.com 时,浏览器会携带这个 Cookie。shop.app.com 应用使用相同的共享密钥环来解密和验证该 Cookie,从而识别出已登录的用户。
最佳实践与常见陷阱#
- 始终使用 HTTPS:共享认证 Cookie 涉及敏感信息在子域名间传输,必须使用 HTTPS 并设置
Cookie.SecurePolicy = CookieSecurePolicy.Always。 - 统一的
ApplicationName:这是最常见的错误来源。请仔细检查所有应用的SetApplicationName值是否完全一致(区分大小写)。 - 密钥环存储的权限:确保所有应用实例对共享的存储(文件共享、Redis、数据库)都有读写权限。
- 密钥生命周期管理:Data Protection 密钥会定期自动轮换。共享存储确保了新旧应用都能访问当前和历史密钥,从而平滑处理已发出的 Cookie。
- 环境隔离:为开发、测试、生产环境使用不同的共享存储位置或不同的
ApplicationName。切勿让生产环境应用使用开发环境的密钥环。 - 测试:部署后,在一个站点登录,然后直接访问另一个站点的受保护页面,而不是登录页面,以验证 SSO 是否真正生效。
总结#
ASP.NET Core 强大的 Data Protection 系统是安全性的基石,但在跨应用共享认证 Cookie 的场景下,其默认的、隔离的密钥环配置会成为障碍。解决此问题的关键在于:
- 理解根源:认识到是密钥环的不一致导致 Cookie 无法被互相验证。
- 配置共享存储:使用文件系统、Redis 等方案,让所有应用访问同一个密钥环。
- 设置统一应用名:使用
SetApplicationName确保应用在共享存储中属于同一个“信任组”。
通过正确配置 Data Protection,你可以轻松地在微服务架构中构建安全、无缝的单点登录体验。