ASP.NET Core MVC中如何在全应用内持久存储用户数据
问题描述
- 正在开发卖家库存与订单管理系统,目标是登录后缓存用户数据,仅在必要时调用API,减少请求次数。
- 不想使用Identity,因为后端API未采用该框架,会增加不必要的复杂度。
- 最初尝试用单例
UserService的静态属性存储用户,发现会导致所有应用实例共享同一用户数据,存在冲突。 - 转而尝试会话和认证机制,但对原理不熟悉,目前将用户数据存入
HttpContext后,在其他控制器调用服务时数据丢失。
当前UserService代码
using Market.Data.Models; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Caching.Memory; using System.Net; using System.Security.Claims; using System.Text; using System.Text.Json; using System.Diagnostics; namespace Market.Services { public class UserService : IUserService { private readonly IHttpClientFactory factory; private readonly IHttpContextAccessor httpContextAccessor; private readonly HttpClient client; private User? User; public UserService(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor) { factory = httpClientFactory; client = factory.CreateClient(); client.BaseAddress = new Uri("https://farmers-market.sommee.com/api/"); this.httpContextAccessor = httpContextAccessor; User = GetUser(); } private async Task SaveUserToContext(User user) { var claims = new List<Claim> { new Claim(ClaimTypes.UserData, JsonSerializer.Serialize(user)), }; var claimsIdentity = new ClaimsIdentity( claims, CookieAuthenticationDefaults.AuthenticationScheme); httpContextAccessor.HttpContext.User = new ClaimsPrincipal(claimsIdentity); } public async Task<User> Login(string email, string password) { var url = $"https://farmers-market.somee.com/api/users/login?email={email}&password={password}"; var response = await client.GetAsync(url); var result = new User(); if (response.IsSuccessStatusCode) { var stringResponse = await response.Content.ReadAsStringAsync(); Console.WriteLine(stringResponse); result = JsonSerializer.Deserialize<User>(stringResponse, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); User = result; await SaveUserToContext(result!); } else { throw new HttpRequestException(response.ReasonPhrase); } if (result == null) { throw new Exception("Error with login"); } return result; } public async Task<HttpStatusCode> Register(User user) { var url = $"https://farmers-market.somee.com/api/users/add"; var jsonParsed = JsonSerializer.Serialize<User>(user, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); HttpContent content = new StringContent(jsonParsed.ToString(), Encoding.UTF8, "application/json"); var response = await client.PostAsync(url, content); return response.StatusCode; } public Task RemoveOrderAsync(int orderId) { if (User == null) { throw new Exception("User is not authenticated"); } User.SoldOrders.Remove(User.SoldOrders.Single(x => x.Id == orderId)); return Task.CompletedTask; } public void AddApprovedOrder(int id) { User!.SoldOrders.Single(x => x.Id == id).IsApproved = true; } public void AddDeliveredOrder(int id) { User!.SoldOrders.Single(x => x.Id == id).IsDelivered = true; } public User? GetUser() { var saved = httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.UserData); if (saved == null) return null; User = JsonSerializer.Deserialize<User>(saved); return User; } } }
当前Program.cs代码
using Market.Services; using Market.Services.Firebase; using Market.Services.Inventory; using Market.Services.Offers; using Market.Services.Orders; using Market.Services.Reviews; using Microsoft.AspNetCore.Authentication.Cookies; using Market.Data; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Market.Data.Models; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddHttpClient(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); // Adjust timeout as needed options.Cookie.HttpOnly = true; options.Cookie.Name = "SessionCookie_" + Guid.NewGuid().ToString(); options.Cookie.IsEssential = true; }); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { // Set a unique cookie name for this instance of the app options.Cookie.Name = "AuthCookie_" + Guid.NewGuid().ToString(); // Or use another unique value }); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IInventoryService, InventoryService>(); builder.Services.AddScoped<IOfferService, OfferService>(); builder.Services.AddScoped<IFirebaseServive, FirebaseService>(); builder.Services.AddScoped<IOrdersService, OrdersService>(); builder.Services.AddScoped<IReviewsService, ReviewsService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseSession(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();
解决方案与优化思路
1. 修复Cookie认证的持久化问题
你当前的SaveUserToContext仅修改了当前请求的HttpContext.User,未将认证信息写入Cookie,导致后续请求无法读取。改用SignInAsync完成会话持久化:
修改SaveUserToContext方法:
private async Task SaveUserToContext(User user) { var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.UserData, JsonSerializer.Serialize(user)) }; var claimsIdentity = new ClaimsIdentity( claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30), IsPersistent = false, AllowRefresh = true }; // 将认证信息写入Cookie await httpContextAccessor.HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); }
同时移除服务类的私有User字段,所有用户数据读取都从HttpContext获取:
// 移除私有字段:private User? User; public User? GetUser() { var httpContext = httpContextAccessor.HttpContext; if (httpContext == null || !httpContext.User.Identity.IsAuthenticated) return null; var saved = httpContext.User.FindFirstValue(ClaimTypes.UserData); return saved == null ? null : JsonSerializer.Deserialize<User>(saved); } // 示例:修改RemoveOrderAsync方法 public Task RemoveOrderAsync(int orderId) { var user = GetUser(); if (user == null) throw new Exception("User is not authenticated"); user.SoldOrders.Remove(user.SoldOrders.Single(x => x.Id == orderId)); // 注意:本地修改后需同步到API,并重新更新Cookie中的用户数据 return Task.CompletedTask; }
2. 改用Session存储用户数据(更简单的方案)
若不想用Cookie认证,可直接用Session存储,逻辑更直观:
修改Login中的保存逻辑:
public async Task<User> Login(string email, string password) { // ... 原有API调用逻辑 ... if (response.IsSuccessStatusCode) { var stringResponse = await response.Content.ReadAsStringAsync(); result = JsonSerializer.Deserialize<User>(stringResponse, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); // 存入Session httpContextAccessor.HttpContext.Session.SetString("CurrentUser", JsonSerializer.Serialize(result)); } // ... 原有异常处理逻辑 ... }
修改GetUser方法:
public User? GetUser() { var httpContext = httpContextAccessor.HttpContext; if (httpContext == null) return null; var savedUser = httpContext.Session.GetString("CurrentUser"); return string.IsNullOrEmpty(savedUser) ? null : JsonSerializer.Deserialize<User>(savedUser); }
同时调整Program.cs中间件顺序(UseSession需在UseRouting后、UseAuthorization前):
app.UseRouting(); app.UseSession(); app.UseAuthentication(); app.UseAuthorization();
3. 同步本地修改到后端API
当你在本地修改用户数据(如标记订单已完成),需执行以下步骤:
- 调用API将修改同步到后端
- 更新本地缓存的用户数据
- 重新将更新后的数据存入Cookie/Session
示例修改AddApprovedOrder方法:
public async Task AddApprovedOrder(int id) { var user = GetUser(); if (user == null) throw new Exception("User is not authenticated"); // 1. 调用API更新订单状态 var response = await client.PostAsync($"api/orders/{id}/approve", null); response.EnsureSuccessStatusCode(); // 2. 更新本地缓存 var order = user.SoldOrders.Single(x => x.Id == id); order.IsApproved = true; // 3. 重新保存到缓存(以Cookie为例) if (httpContextAccessor.HttpContext != null) { var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.UserData, JsonSerializer.Serialize(user)) }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await httpContextAccessor.HttpContext.SignInAsync(new ClaimsPrincipal(identity)); } }
4. 核心避坑原则
- 绝对不要用静态字段存储用户请求上下文数据
- 用户服务必须注册为
Scoped(每个请求实例化一次),禁止用Singleton - 所有用户数据读取都应从
HttpContext(Session/Cookie)获取,而非服务类私有字段
内容的提问来源于stack exchange,提问作者Gabriel Goranov
相关产品推荐
相关产品推荐

