如何在多个 ASP.NET MVC 3 项目间共享购物车?
结论
不要把购物车本身存入 Cookie、HttpContext 或 ASP.NET Session。建议采用下面的方案:
- 将购物车及其明细统一存入数据库。
- 浏览器 Cookie 只保存一个不可预测的购物车标识
CartId。 - 各个 MVC 应用通过统一的购物车服务或数据访问层,根据
CartId读取和修改购物车。 - 注册用户登录后,将匿名购物车关联到用户账号,或与账号下已有的购物车合并。
- 部署多台服务器时,让所有节点访问同一份购物车存储,不依赖服务器本地内存。
这套设计可以处理匿名用户、注册用户、多个 IIS Application,也适用于以后的负载均衡部署。
为什么不应该依赖 HttpContext
HttpContext 表示当前 HTTP 请求的上下文,不是跨请求的持久化存储。
其中几种常见状态需要分清:
HttpContext.Items:只在当前请求期间有效,请求结束后内容就会消失。HttpContext.Session:属于当前 ASP.NET Application 的会话状态,不等于多个应用共享的购物车。HttpContext.Request.Cookies:可以读取浏览器携带的购物车标识,但不适合保存完整购物车。
如果 /cart、/permit 和 /billing 在 IIS 中被配置为不同的 Application,它们就有各自的应用边界。即使这些应用位于同一个网站和域名下,也不能假定它们会自动共享 Session 或其他 HttpContext 状态。
因此,可以通过 HttpContext 取得当前请求中的 CartId,但不能用它存储需要跨应用共享的购物车。
为什么不建议把完整购物车放入 Cookie
Cookie 可以在同一域名的多个路径之间共享,但其中应该只保存购物车标识,不要保存价格、商品明细或应付金额。
把完整购物车放入 Cookie 会产生以下问题:
- Cookie 会随每次请求发送。购物车越大,请求开销越高。
- 客户端可以删除 Cookie,也可能尝试篡改其中的内容。
- 商品价格、罚款金额和许可证费用可能变化。
- Cookie 有容量限制。
- 服务端难以进行并发控制、审计和状态校验。
- 系统无法可靠阻止客户端提交过期或伪造的金额。
即使 Cookie 已加密或签名,结算时也必须由服务端重新计算应付金额。
推荐架构
可以把购物车做成独立的共享组件:
Browser
│
│ Cookie: egov_cart=<random-id>
│
├── /permit
├── /billing
└── /cart
│
▼
Shared Cart Service
│
▼
SQL Database
各业务应用负责提供可购买项目,并调用购物车接口。购物车服务负责:
- 创建购物车;
- 添加、删除和更新购物车项目;
- 关联注册用户;
- 合并匿名购物车;
- 重新计算或验证金额;
- 设置过期时间;
- 控制并发修改;
- 支付成功后关闭购物车。
如果几个项目由同一团队开发,部署关系也比较简单,可以先采用共享类库和统一数据库。等到需要单独扩容或隔离时,再把它改为内部 HTTP 服务。重点是集中存储购物车状态,不必一开始就拆成微服务。
数据模型示例
数据库至少可以包含购物车和购物车项目两张表:
CREATE TABLE ShoppingCart
(
CartId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
UserId NVARCHAR(128) NULL,
Status NVARCHAR(20) NOT NULL,
CreatedUtc DATETIME NOT NULL,
UpdatedUtc DATETIME NOT NULL,
ExpiresUtc DATETIME NOT NULL,
RowVersion ROWVERSION NOT NULL
);
CREATE TABLE ShoppingCartItem
(
CartItemId BIGINT IDENTITY PRIMARY KEY,
CartId UNIQUEIDENTIFIER NOT NULL,
SourceSystem NVARCHAR(50) NOT NULL,
ItemType NVARCHAR(50) NOT NULL,
ReferenceId NVARCHAR(200) NOT NULL,
Description NVARCHAR(500) NOT NULL,
Quantity INT NOT NULL,
DisplayAmount DECIMAL(18, 2) NULL,
CONSTRAINT FK_ShoppingCartItem_Cart
FOREIGN KEY (CartId) REFERENCES ShoppingCart(CartId)
);
ReferenceId 应指向许可证申请、账单、罚单等业务记录。在政府缴费场景中,购物车显示的金额不能作为最终收费依据。进入结算阶段后,系统需要向对应的业务模块重新确认:
- 项目是否仍可支付;
- 是否已经通过其他渠道支付;
- 当前欠款是否发生变化;
- 是否有滞纳金、折扣或其他调整。
RowVersion 可以用于乐观并发控制,防止两个请求同时修改购物车时相互覆盖。
共享购物车 Cookie
如果所有应用都位于同一个主机名,例如 egov.domain.gov,通常只需把 Cookie 的 Path 设置为 /。一般不必设置 Domain,因为 host-only Cookie 的作用范围更小,也更安全。
下面的代码可以放在各项目共同引用的类库中:
using System;
using System.Web;
public static class CartCookie
{
private const string CookieName = "egov_cart";
public static Guid? Read(HttpRequestBase request)
{
HttpCookie cookie = request.Cookies[CookieName];
Guid cartId;
if (cookie != null && Guid.TryParse(cookie.Value, out cartId))
{
return cartId;
}
return null;
}
public static void Write(
HttpResponseBase response,
Guid cartId,
DateTime expiresUtc)
{
var cookie = new HttpCookie(CookieName, cartId.ToString("N"))
{
HttpOnly = true,
Secure = true,
Path = "/",
Expires = expiresUtc
};
response.Cookies.Add(cookie);
}
public static void Delete(HttpResponseBase response)
{
var cookie = new HttpCookie(CookieName, String.Empty)
{
HttpOnly = true,
Secure = true,
Path = "/",
Expires = DateTime.UtcNow.AddDays(-1)
};
response.Cookies.Add(cookie);
}
}
Secure = true 表示 Cookie 只通过 HTTPS 发送。处理公民身份、账单和支付信息的系统应全站使用 HTTPS。
Guid 可以作为购物车的查找标识,但不能代替权限校验。请求中带有有效的 CartId,不代表系统可以直接展示敏感账单信息或付款人资料。系统还要验证用户身份和购物车所有权,匿名购物车则可以使用额外的高熵访问令牌。
在控制器中取得购物车
可以通过统一服务封装读取 Cookie、查找购物车,以及在必要时创建购物车的过程:
public interface ICartService
{
ShoppingCart GetOrCreate(Guid? cartId, string userId);
void AddItem(Guid cartId, CartItem item);
void Merge(Guid anonymousCartId, string userId);
}
控制器示例:
public class PermitController : Controller
{
private readonly ICartService _cartService;
public PermitController(ICartService cartService)
{
_cartService = cartService;
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddToCart(string permitId)
{
Guid? cartId = CartCookie.Read(Request);
string userId = User.Identity.IsAuthenticated
? User.Identity.Name
: null;
ShoppingCart cart = _cartService.GetOrCreate(cartId, userId);
_cartService.AddItem(
cart.CartId,
new CartItem
{
SourceSystem = "Permit",
ItemType = "PermitFee",
ReferenceId = permitId,
Quantity = 1
});
CartCookie.Write(Response, cart.CartId, cart.ExpiresUtc);
return Redirect("/cart");
}
}
GetOrCreate 不能只检查 Cookie 是否存在,还要查询数据库并确认:
- 购物车是否存在;
- 是否已经过期;
- 当前状态是否允许编辑;
- 是否属于当前登录用户;
- 匿名访问凭据是否有效。
注册用户和匿名用户如何衔接
匿名用户第一次添加项目时,处理流程如下:
- 服务端创建购物车。
- 生成随机
CartId或单独的访问令牌。 - 将标识写入
Path为/的 Cookie。 - 暂时将购物车记录的
UserId留空。
用户随后登录时,系统需要明确执行购物车关联逻辑:
- 如果账号没有活动购物车,将匿名购物车关联到该账号。
- 如果账号已有购物车,按照业务规则合并两辆购物车。
- 如果有重复的账单或罚单,进行去重,不要直接累加。
- 对于已经支付或无法支付的项目,将其从购物车删除,或提示用户处理。
- 合并完成后,废弃原来的匿名购物车标识,并更新 Cookie。
退出登录后是否保留购物车,要根据隐私要求和业务规则决定。如果购物车中含有敏感账单信息,通常不应只凭一个长期 Cookie,就在公共电脑上重新展示全部详情。
Session State Server 是否可用
问题中提到的 “state machine” 可能是指 ASP.NET State Server,也就是进程外 Session 状态服务。它并不是状态机。
ASP.NET State Server 或 SQL Server Session State 可以让单个应用在多个进程或多台服务器之间共享 Session,但不能自动让多个独立的 ASP.NET Application 共享业务状态。不同应用通常有各自的 Session 命名空间和生命周期。
通过统一配置、自定义 Session Provider 或其他特殊处理,理论上可以实现跨应用 Session。不过,这会让购物车依赖 ASP.NET Session 机制,并产生一些限制:
- 应用边界和配置会更复杂;
- Session 到期后,购物车会直接丢失;
- 后台任务和其他服务难以访问购物车;
- 审计、查询和恢复能力较弱;
- 部署或配置错误容易引发难以排查的问题;
- 支付流程的业务状态不适合只存放在临时 Session 中。
Session 可以缓存少量非关键数据,但不应作为购物车的权威存储。
多服务器部署
将购物车存入数据库后,VMWare 和多服务器部署不会改变整体设计。所有 Web 节点都从共享存储中读取购物车,也就不需要粘性会话:
Load Balancer
├── Web Server A
├── Web Server B
└── Web Server C
│
▼
Shared Database
如果以后访问量增大,可以加入 Redis 等分布式缓存,但需要恢复、审计或完成支付的状态仍应保存在数据库中。即使缓存失效,系统也必须能从持久化存储中重新构建购物车。
多服务器环境还需要处理以下事项:
- 使用 UTC 保存时间;
- 原子更新购物车;
- 处理乐观并发冲突;
- 防止重复提交;
- 保证支付回调的幂等性;
- 定期清理过期购物车;
- 支付成功后禁止继续修改购物车;
- 制定数据库或支付接口暂时不可用时的恢复策略。
多个应用之间的身份共享
共享购物车和共享登录是两个不同的问题。
如果多个 MVC 应用还要共享 Forms Authentication,通常需要统一认证 Cookie 的名称、路径、域和加密配置,例如使用一致的 machineKey。实际配置取决于认证方式和 .NET 版本。部署共享购物车并不会自动带来单点登录。
即使认证 Cookie 不共享,匿名购物车仍然可以通过作用于 / 的购物车 Cookie 工作。登录后关联购物车时,应使用稳定且不可变的内部用户 ID,不要使用显示名称或电子邮件地址。
支付场景的额外注意事项
购物车只是待支付项目的集合,不能直接充当财务账本。提交支付前,系统应创建不可随意修改的支付订单或结算快照,并在服务端完成最终校验。
至少应满足以下要求:
- 不信任客户端提交的价格和总金额;
- 每次结算时重新验证业务项目;
- 使用
decimal保存货币金额,不使用float或double; - 为支付请求设置唯一幂等键;
- 记录订单金额、项目明细和计算依据;
- 支付成功后,通过事务或可靠的补偿流程更新各业务模块;
- 购物车过期不代表可以删除相关财务记录;
- 不在 Cookie 中保存姓名、账号、罚单内容等敏感信息;
- 所有修改操作都要执行 CSRF 防护和授权检查。
综合这些要求,建议采用 “共享购物车服务或存储 + 浏览器中的不透明购物车标识”。这种方案不依赖某一台服务器、某一个 IIS Application 或某一次 ASP.NET Session,可以支持匿名访问、登录后合并购物车、多个业务项目和以后的横向扩展。
备注:内容仅供参考。
