You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

当你在本地修改用户数据(如标记订单已完成),需执行以下步骤:

  1. 调用API将修改同步到后端
  2. 更新本地缓存的用户数据
  3. 重新将更新后的数据存入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 06:25:56