【问题标题】:Ajax Calls Return 401 When .NET Core Site Is Deployed部署 .NET Core 站点时,Ajax 调用返回 401
【发布时间】:2020-09-23 10:37:34
【问题描述】:

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

不起作用:

作品:

对我来说奇怪的是另一个用户可以登录,甚至我可以在新发布后偶尔登录,并且在正确设置 cookie 的情况下让这些 ajax 调用正常工作。

这里有一些更具体的代码。不确定是不是我在 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(加载页面时运行上述 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>

编辑:

我发现的是带有“.AspNetCore.Antiforgery”、“.AspNetCore.Identity.Application”和“.AspNetCore.Session”属性的“Cookie”标头在运行时始终在 ajax 请求中正确设置本地。部署时,它只设置带有 session 属性的 cookie。我在我的 Startup.cs 中找到了一个设置,将 cookie 设置为 HttpOnly: options.Cookie.HttpOnly = true; 这会导致我的问题吗?将其设置为错误的工作吗?如果那不安全,那么我的方法有哪些变通方法/替代方法。我仍然需要实现用户身份验证的基本原理,并且能够触发 ajax 请求。

另一个编辑:

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

【问题讨论】:

  • 很难说。但是由于 Firefox 表现良好而 Chrome 不知道它是否与 Chrome 中的相同站点更改有关?他们停止了推出(因为电晕),但一些版本已经实现了它。您能否尝试在您的配置服务中指定 options.Cookie.SameSite = SameSiteMode.None; (您已经设置 options.Cookie.HttpOnly = true;)
  • 在(当时)最新的 Chrome 更新后的几周前遇到了非常相似的事情。正如您(和其他人)所建议的那样,这完全是关于客户端上 cookie 处理的差异。
  • 当你部署你有相同的URL?尝试 always 在 Incognito 浏览器模式下运行测试,我怀疑浏览器会将 dev cookie 发送到 prod服务器。

标签: c# .net ajax asp.net-core asp.net-core-identity


【解决方案1】:

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

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

请参阅this 了解更多信息。

我还在你的 Startup 中看到了这部分,这增加了我猜测的机会:

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

在您的开发环境中,http 上一切正常。但是在部署环境中https进来,如果一些请求去http,一些去https,一些cookie没有返回,你可以面对这个问题。

【讨论】:

  • 我相信这是最能解释我所看到的行为的事情。我添加了 app.UseHttpsRedirection();回到我在 Startup 中的 Configure 方法(我之前有它,但由于某种原因把它拿出来了)。我重新部署到我的生产服务器,它工作得很好。希望这能永久解决问题。
  • 另外,我正在通过myserver/myapp 加载网站。从技术上讲,它是位于我们的防火墙后面且不对外暴露的登台服务器。未配置安全证书,因此我通过 http 加载以避免“风险”页面。发布后,我通过 https 加载,接受了“风险”,它工作得很好。
【解决方案2】:

如您所见,这是各种浏览器中的 ajax 调用差异。服务器端编程工作正常,不能随意响应,除非它面临来自浏览器的不同请求(这里是 google chome)。我相信在 ajax 调用中使用断言应该可以解决像使用withcredentials : true 这样的问题。让我知道问题是否仍然存在。

【讨论】:

    【解决方案3】:

    这看起来像是一个会话管理问题,使用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
    });
    

    【讨论】:

      猜你喜欢
      • 2018-07-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-12
      • 1970-01-01
      相关资源
      最近更新 更多