Ajax 在部署 .NET Core 站点时调用 Return 401

Ajax Calls Return 401 When .NET Core Site Is Deployed

我有一个 st运行ge 情况,我无法始终如一地复制。我有一个在 .NET Core 3.0 中开发的 MVC 网站,并使用 .NET Core Identity 授权用户。当我 运行 在本地开发环境中的站点时,一切正常 (经典 "works on my machine!")。当我将它部署到我的登台 Web 服务器时,我开始看到问题。用户可以登录成功,通过身份验证,并重定向到主页。注意:除了处理身份验证的控制器外,所有控制器都用 [Authorize] 属性和 [AutoValidateAntiforgeryToken] 属性修饰。主页加载得很好。但是,有几个 ajax 调用 运行 当页面加载时回调到 Home 控制器以加载一些条件数据并检查是否有一些 Session 级别变量已经被加载设置了。 这些 ajax 调用 return 401 未经授权。问题是我无法让这种行为始终如一地重复。我实际上有另一个用户同时登录(同一个应用程序,同一个服务器)并且它对他们来说工作得很好。我在 Chrome 中打开了开发人员控制台,并将我认为的问题归结为一个常见(或不常见)的因素。有效的调用(例如加载主页,或对其他用户成功的 ajax 调用)具有“.AspNetCore.Antiforgery”、“.AspNetCore.Identity.Application”和“.AspNetCore.Session”请求中设置的 cookies headers。不起作用的调用(我的 ajax 调用)仅设置了“.AspNetCore.Session”cookie。另一件需要注意的事情是,这种行为发生在网站上的每个 ajax 调用中。通过导航或表单发布对控制器操作进行的所有调用都可以正常工作。

无效:

作品:

对我来说 st运行ge 是另一个用户可以登录,甚至我可以在新发布后偶尔登录,并且让那些 ajax 调用与 cookie 一起工作设置正确。

下面是一些更具体的代码。不确定是不是我在 Identity 或 Session 配置上设置了错误。

Startup.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(前面提到的 运行s 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>

编辑:

我发现 "Cookie" header 带有“.AspNetCore.Antiforgery”、“.AspNetCore.Identity.Application”和“.AspNetCore.Session” " 当 运行 在本地时,属性总是在 ajax 请求中正确设置。部署时,它只设置带有 session 属性的 cookie。我在我的 Startup.cs 中找到了一个设置,将 cookie 设置为 HttpOnly: options.Cookie.HttpOnly = true; 这会导致我的问题吗?将其设置为虚假工作吗?如果那不安全,我的方法有哪些 work-arounds/alternate 方法。我仍然需要实施用户身份验证的基本原则并能够触发 ajax 请求。

另一个编辑:

今天,在我再次部署站点后,我 运行 站点同时在 Firefox 和 Chrome 中运行。 Firefox 在验证后发送了正确的 cookie,运行 正常。但是,Chrome 仍然显示 401 行为。

在我看来,您的问题可能是因为 cookie 在 http 和 https 场景中的不同行为!

设置为 https 模式的安全 cookie 在回发到 http 时无法检索。

有关详细信息,请参阅 this

我在你的启动中也看到了这部分,这增加了我猜测的机会:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

在您的开发环境中,http 一切正常。但是在部署环境中 https 进来,如果一些请求去 http 而一些去 https,一些 cookies 不 return 你可能会遇到这个问题。

如您所见,这是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 的会话 cookie。这些都是为了帮助开发人员遵守 GDPR 法规。

例如在您的应用程序中,作为可用选项之一,您可以使会话 cookie 必不可少,以使其甚至在用户接受 cookie 条款之前就可以写入,即

services.AddSession(options => 
{
    options.Cookie.IsEssential = true; // make the session cookie Essential
});