我有一个奇怪的情况,我无法持续地复制。我有一个用.NET Core 3.0开发的MVC网站,并且用.NET Core Identity授权用户。当我在本地的开发环境中运行该网站时,一切都很正常。(经典的 "在我的机器上工作!"). 当我把它部署到我的暂存Web服务器时,我开始看到这个问题。用户可以成功登录,被认证,并重定向到主页。注意:所有的控制器,除了处理认证的控制器之外,都是以 [Authorize]
属性和 [AutoValidateAntiforgeryToken]
属性。主页加载得很好。然而,有几个ajax调用在页面加载时运行,回调到Home控制器加载一些条件数据,并检查一些Session级别的变量是否已经设置。这些ajax调用返回了一个401 Unauthorized. 问题是我不能让这种行为持续重复。实际上,我让另一个用户同时登录(同一个应用程序,同一个服务器),他们的工作也很正常。我打开Chrome浏览器中的开发者控制台,将我认为的问题归结为一个共同(或不共同)的因素。工作的调用(如加载主页,或对其他用户成功的ajax调用)在请求头中设置了".AspNetCore.Antiforgery"、".AspNetCore.Identity.Application "和".AspNetCore.Session "cookies。不工作的调用(我的ajax调用)只有".AspNetCore.Session "cookie被设置。另外需要注意的是,这种行为发生在网站上的每个ajax调用中。所有通过导航或表单发布对控制器动作的调用都能正常工作。
对我来说,奇怪的是,另一个用户可以登录,甚至我可以登录后,偶尔一个新的发布,并有这些ajax调用工作就好了正确设置的Cookie。
下面是一些代码,说得更具体一点。不知道是我的身份或会话配置出了问题。
启动.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Env { get; set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<User, UserRole>(options =>
{
options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<QCAuthorizationContext>()
.AddDefaultTokenProviders(); ;
services.AddDbContext<QCAuthorizationContext>(cfg =>
{
cfg.UseSqlServer(Configuration.GetConnectionString("Authorization"));
});
services.AddSingleton<IConfiguration>(Configuration);
services.AddControllersWithViews();
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromHours(4);
options.Cookie.HttpOnly = true;
// Make the session cookie essential
options.Cookie.IsEssential = true;
});
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
});
services.ConfigureApplicationCookie(options =>
{
//cookie settings
options.ExpireTimeSpan = TimeSpan.FromHours(4);
options.SlidingExpiration = true;
options.LoginPath = new Microsoft.AspNetCore.Http.PathString("/Account/Login");
});
services.AddHttpContextAccessor();
//services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
IMvcBuilder builder = services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "auth4",
pattern: "{controller=Account}/{action=Authenticate}/{id?}");
});
}
}
登录控制器动作
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel iViewModel)
{
ViewBag.Message = "";
try
{
var result = await signInManager.PasswordSignInAsync(iViewModel.Email, iViewModel.Password, false, false);
if (result.Succeeded)
{
var user = await userManager.FindByNameAsync(iViewModel.Email);
if (!user.FirstTimeSetupComplete)
{
return RedirectToAction("FirstLogin");
}
return RedirectToAction("Index", "Home");
}
else
{
ViewBag.Message = "Login Failed.";
}
}
catch (Exception ex)
{
ViewBag.Message = "Login Failed.";
}
return View(new LoginViewModel() { Email = iViewModel.Email });
}
家庭控制器
public class HomeController : BaseController
{
private readonly ILogger<HomeController> _logger;
public HomeController(IConfiguration configuration, ILogger<HomeController> logger, UserManager<User> iUserManager) : base(configuration, iUserManager)
{
_logger = logger;
}
public async Task<IActionResult> Index()
{
HomeViewModel vm = HomeService.GetHomeViewModel();
vm.CurrentProject = HttpContext.Session.GetString("CurrentProject");
vm.CurrentInstallation = HttpContext.Session.GetString("CurrentInstallation");
if (!string.IsNullOrEmpty(vm.CurrentProject) && !string.IsNullOrEmpty(vm.CurrentInstallation))
{
vm.ProjectAndInstallationSet = true;
}
return View(vm);
}
public IActionResult CheckSessionVariablesSet()
{
var currentProject = HttpContext.Session.GetString("CurrentProject");
var currentInstallation = HttpContext.Session.GetString("CurrentInstallation");
return Json(!string.IsNullOrEmpty(currentProject) && !string.IsNullOrEmpty(currentInstallation));
}
public IActionResult CheckSidebar()
{
try
{
var sidebarHidden = bool.Parse(HttpContext.Session.GetString("SidebarHidden"));
return Json(new { Success = sidebarHidden });
}
catch (Exception ex)
{
return Json(new { Success = false });
}
}
}
基础控制器
[AutoValidateAntiforgeryToken]
[Authorize]
public class BaseController : Controller
{
protected IConfiguration configurationManager;
protected SQLDBContext context;
protected UserManager<User> userManager;
public BaseController(IConfiguration configuration, UserManager<User> iUserManager)
{
userManager = iUserManager;
configurationManager = configuration;
}
public BaseController(IConfiguration configuration)
{
configurationManager = configuration;
}
protected void EnsureDBConnection(string iProject)
{
switch (iProject)
{
case "A":
DbContextOptionsBuilder<SQLDBContext> AOptionsBuilder = new DbContextOptionsBuilder<SQLDBContext>();
AOptionsBuilder.UseLazyLoadingProxies().UseSqlServer(configurationManager.GetConnectionString("A"));
context = new SQLDBContext(AOptionsBuilder.Options);
break;
case "B":
DbContextOptionsBuilder<SQLDBContext> BOptionsBuilder = new DbContextOptionsBuilder<SQLDBContext>();
BOptionsBuilder.UseLazyLoadingProxies().UseSqlServer(configurationManager.GetConnectionString("B"));
context = new SQLDBContext(BOptionsBuilder.Options);
break;
case "C":
DbContextOptionsBuilder<SQLDBContext> COptionsBuilder = new DbContextOptionsBuilder<SQLDBContext>();
COptionsBuilder.UseLazyLoadingProxies().UseSqlServer(configurationManager.GetConnectionString("C"));
context = new SQLDBContext(COptionsBuilder.Options);
break;
}
}
}
_Layout.cshtml Javascript (当页面加载时,运行上述的ajax调用)
<script type="text/javascript">
var afvToken;
$(function () {
afvToken = $("input[name='__RequestVerificationToken']").val();
$.ajax({
url: VirtualDirectory + '/Home/CheckSidebar',
headers:
{
"RequestVerificationToken": afvToken
},
complete: function (data) {
console.log(data);
if (data.responseJSON.success) {
toggleSidebar();
}
}
});
$.ajax({
url: VirtualDirectory + '/Home/CheckSessionVariablesSet',
headers:
{
"RequestVerificationToken": afvToken
},
complete: function (data) {
console.log(data);
if (data.responseJSON) {
$('#sideBarContent').attr('style', '');
}
else {
$('#sideBarContent').attr('style', 'display:none;');
}
}
});
$.ajax({
url: VirtualDirectory + '/Account/UserRoles',
headers:
{
"RequestVerificationToken": afvToken
},
complete: function (data) {
if (data.responseJSON) {
var levels = data.responseJSON;
if (levels.includes('Admin')) {
$('.adminSection').attr('style', '');
}
else {
$('.adminSection').attr('style', 'display:none;');
}
}
}
});
});
</script>
EDIT:
我发现,当本地运行时,带有".AspNetCore.Antiforgery"、".AspNetCore.Identity.Application "和".AspNetCore.Session "属性的 "Cookie "头总是在ajax请求中正确设置。当部署时,它只设置了session属性的cookie。我发现我在我的".AspNetCore.Identity.Application "中设置了一个 启动.cs 将cookie设置为 HttpOnly: options.Cookie.HttpOnly = true;
会不会是这个原因导致我的问题?设置为false可以吗?如果这样不安全,有什么变通的方法可以替代我的方法。我仍然需要实现用户认证的基本原则,并且能够触发ajax请求。
另一个编辑。
今天在我再次部署网站后,我同时在Firefox和Chrome中运行了这个网站。Firefox在认证后发送了正确的cookie,并且运行正常。但是,Chrome仍然显示401行为。
在我看来,你的问题可能是因为在http与https场景下,cookie的行为不同!
安全的Cookie,设置在 https
模式下,贴回时无法找回。http
.
见 这个 以获取更多信息。
我在你的Startup中也看到了这部分,这增加了我猜测的机会。
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
在你的开发环境中,所有的东西都可以在 http
. 但在部署环境中 https
来的,如果有些请求到 http
和一些人去 https
,有些cookie不返回,你可以面对这个问题。
正如你所发现的。这就是ajax调用在各种浏览器中的区别. 服务器端编程工作正常,不能随意响应,除非它面对来自浏览器的不同请求(这里是google chome)。我相信在ajax调用中使用断言应该可以解决这个问题,就像使用 withcredentials : true
. 如果问题仍然存在,请告诉我。
这看起来像是一个会话管理问题,使用的是 services.AddDistributedMemoryCache()
有时会带来会话问题,尤其是在共享主机环境下。你是否可以尝试缓存到数据库。
例如
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = connectionString;
options.SchemaName = "dbo";
options.TableName = "DistributedCache";
});
确保你处理 GDPR
问题,这影响了.Net core > 2.0的session cookie。这些问题的出现是为了帮助开发者符合GDPR的规定。
例如,在您的应用程序中,作为可用的选项之一,您可以使会话cookie必不可少,以使其在用户接受cookie条款之前就被写入,即。
services.AddSession(options =>
{
options.Cookie.IsEssential = true; // make the session cookie Essential
});