无法使用 ASP.NET CORE 2.2 为本地化生成响应 cookie

Can't generate a Response cookie for localization using ASP.NET CORE 2.2

我不会骗你们的。我对这个问题束手无策。我已经浪费了大约 4 个小时的时间来尝试书中的每个解决方案来解决一个问题,我知道这个问题对于试图将本地化引入其 Web 应用程序的程序员来说非常普遍。每当我尝试将我的网页的文化从英语 (en-US) 更改为韩语 (ko-KR) 时,它默认设置为默认设置为英语。我已经缩小了问题的范围,我知道我没有生成正确的响应 cookie,但是 none 我在网上找到的解决方案显然 非常 常见问题有所帮助我。

我尝试刷新 cookie 和缓存,我添加了 Microsoft.AspNetCore.Localization;Microsoft.Extensions.Localization; 扩展,我尝试使用 isEssential 参数作为 CookieOptions 对象,我知道我的文件结构是正确的,并且我所有的 .resx 文件都在它们应该在的位置,因为当我使用 手动将网站切换为韩语时,我能够看到我应该看到的所有翻译?culture=ko-KR,我相信我已经正确配置了我的 startup.cs、控制器文件和部分视图,我需要一条生命线。

我几天前按照在线教程设置了一个虚拟教程 Web 应用程序,并且能够成功更改该 Web 应用程序的区域性。由于 cookie 的工作方式,我只能通过更改虚拟 Web 应用程序上的语言来更改主应用程序上的语言,而这根本不可行。

这是我的代码的重要部分。

startup.cs

            using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Serialization;
using Serilog;
using Snape.DataLayer.Entities;
using Snape.Web.ScheduledProcessor;
using Snape.Web.Services;
using Snape.WebSecurity.Hashing;
using Snape.WebSecurity.Helpers;
using Snape.WebSecurity.Tokens;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Threading.Tasks;
using System.Linq;

namespace Snape.Web
{
    public class Startup
    {
        private readonly IConfiguration _configProvider;
        private readonly SigningConfiguration _signConfig;
        private readonly IConfigurationRoot _constantsConfigProvider;

        public Startup(IConfiguration configuration)
        {
            _configProvider = configuration;
            _signConfig = new SigningConfiguration();

            // Loading Constants.json && Configuration.json
            var configurationBuilder = new ConfigurationBuilder()
                .AddJsonFile($"{_configProvider.GetSection("Constants").Value}", optional: false, reloadOnChange: true)
                .AddJsonFile($"{_configProvider.GetSection("Version").Value}", optional: true, reloadOnChange: true);
            _constantsConfigProvider = configurationBuilder.Build();

            // Initializing Serilog
            Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(config =>
            {
                var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
                config.Filters.Add(new AuthorizeFilter(policy));
            });
            services.AddDistributedMemoryCache(); // Adds a default in-memory implementation of IDistributedCache
            services.AddSession(options => options.IdleTimeout = TimeSpan.FromHours(1));

            /* Note this is commented out.
            var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions()
            {
                Path = "/",
                HttpOnly = false,
                IsEssential = true, //<- there
                Expires = DateTime.Now.AddMonths(1),
            }; */
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => false;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });


            // we need to add localization to the project for views, controllers, and data annotations.
            services.AddMvc()
                // localization options are going to have their resources (language dictionary) stored in Resources folder.
                .AddViewLocalization(opts => { opts.ResourcesPath = "Resources"; })
                .AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix)
                .AddDataAnnotationsLocalization()
              .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            // we are configuring the localization service to support a list of provided cultures.
            services.Configure<RequestLocalizationOptions>(opts =>
            {
                // the list of supported cultures.
                var supportedCultures = new List<CultureInfo>
                {
                    new CultureInfo("en"),
                    new CultureInfo("en-US"),
                    new CultureInfo("ko"),
                    new CultureInfo("ko-KR"),

                };

                // set the localization default culture as english
                opts.DefaultRequestCulture = new RequestCulture("en-US");

                // supported cultures are the supportedCultures variable we defined above.
                // formatiting dates, numbers, etc.
                opts.SupportedCultures = supportedCultures;

                // UI strings that we have localized 
                opts.SupportedUICultures = supportedCultures;

            });


            services.AddMvc().AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
            services.AddDbContext<SnapeDbContext>(options => options.UseLazyLoadingProxies().UseSqlite(_configProvider.GetConnectionString("SnapeDbConnection")));

            services.AddSingleton(_constantsConfigProvider);   // IConfigurationRoot
            // *If* you need access to generic IConfiguration this is **required**
            services.AddSingleton(_configProvider);
            // Background task for data push
            services.AddSingleton<IHostedService, DataPushingTask>();
            // Background task for device's state check
            services.AddSingleton<IHostedService, HeartbeatTask>();
            // Background task for project's sync with cloud
            services.AddSingleton<IHostedService, SyncingTask>();
            // Background task for Purging
            services.AddSingleton<IHostedService, PurgingTask>();
            // Service for Internet Management
            services.AddTransient<InternetService>();

            services.Configure<TokenOptions>(_configProvider.GetSection("TokenOptions"));
            var tokenOptions = _configProvider.GetSection("TokenOptions").Get<TokenOptions>();

            services.AddSingleton<IPassportHasher, PasswordHasher>();
            services.AddSingleton<ITokenHelper, TokenHelper>();
            services.AddSingleton(_signConfig);

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(jwtBearerOptions =>
                {
                    jwtBearerOptions.RequireHttpsMetadata = false;
                    jwtBearerOptions.SaveToken = true;
                    jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = tokenOptions.Issuer,
                        ValidAudience = tokenOptions.Audience,
                        IssuerSigningKey = _signConfig.Key,
                        ClockSkew = TimeSpan.Zero
                    };
                });
            services.Configure<FormOptions>(options =>
            {
                options.ValueCountLimit = int.MaxValue;
                options.ValueLengthLimit = 1024 * 1024 * 100; // 100MB max len form data
            });
            System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-AU");
            System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-AU");
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            // Enabling Logger
            loggerFactory.AddSerilog();

            app.UseHttpsRedirection();
            app.UseStaticFiles();


            // specify that globalization is being used in the pipeline.
            var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
            app.UseRequestLocalization(options.Value);
            app.UseCookiePolicy();

            app.UseSession();
            //Add JWToken to all incoming HTTP Request Header
            app.Use(async (context, next) => {
                var jwToken = context.Session.GetString("JWToken");
                if (!string.IsNullOrEmpty(jwToken))
                {
                    context.Request.Headers.Add("Authorization", "Bearer " + jwToken);
                }
                await next();
            });
            app.UseAuthentication();
            app.UseStatusCodePages(context => {
                var response = context.HttpContext.Response;
                if (response.StatusCode == (int)HttpStatusCode.Unauthorized || response.StatusCode == (int)HttpStatusCode.Forbidden)
                {
                    response.Redirect("/Account/Login");
                    if (Utilities.WebUtility.IsAjaxRequest(context.HttpContext.Request))
                        response.StatusCode = (int)HttpStatusCode.Unauthorized;
                }
                return Task.CompletedTask;
            });
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Account}/{action=Login}/{id?}");
                routes.MapRoute(
                    "invalid_route",
                    "{*url}",
                    new { controller = "NotFound", action = "Index" });
            });
#if RELEASE
            if (_constantsConfigProvider.GetValue<bool>("CELLULAR_ON"))
            {
                Task.Run(async () => { await app.ApplicationServices.GetRequiredService<InternetService>().Enable(); });
            }
#endif
        }
    }
}

AccountController.cs

public class AccountController : BaseController
    {
        // the localizer dictionary to translate languages for this controller.
        readonly IStringLocalizer<AccountController> _localizer;

        readonly IConfiguration _configProvider;
        readonly IPersonFacade _personFacade;
        readonly SnapeDbContext _dbContext;
        readonly ITokenHelper _tokenHelper;

        // AccountController constructor
        public AccountController(IStringLocalizer<AccountController> localizer, SnapeDbContext dbContext, IConfiguration configuration, IPassportHasher passwordHasher, ITokenHelper tokenHandler, 
            IConfigurationRoot constantsConfig) : base(constantsConfig)
        {
             // initialize the localizer.
            _localizer = localizer;
            _dbContext = dbContext;
            _tokenHelper = tokenHandler;
            _configProvider = configuration;

            _personFacade = new PersonFacade(dbContext);
        }

        [HttpPost] // annotation that specifies that this action is called on an HTTPPost
        // this method needs to persist on both this page and any subsequent ones. Sets cookie for changed culture.
        public IActionResult SetLanguage(string culture, string returnURL)
        {
            // set the cookie on the local machine of the Http Response to keep track of the language in question.
            // append the cookie and its language options.
            Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName, // name of the cookie
                CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),  // create a string representation of the culture for storage
                new CookieOptions { Expires = DateTimeOffset.UtcNow.AddDays(1),
                    IsEssential = true, //<- there
                } // expiration after one day.
                );

            return LocalRedirect(returnURL); // redirect to the original URL, the account page.
        }

_SelectLanguagePartial.cshtml

@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@using Microsoft.Extensions.Options
@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> LocOptions
@{
    // this code finds out what cultures I am supporting.
    // it is all defined in startup.cs
    var requestCulture = Context.Features.Get<IRequestCultureFeature>();
    var cultureItems = LocOptions.Value.SupportedUICultures // all the supported cultures.
        .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
        .ToList();

}

<!-- Partial view in ASP.NET MVC is special view which renders a portion of view content. It is just like a user control of a web form application.
    Partial views can be reusable in multiple views. It helps us to reduce code duplication. In other words a partial view enables us to render a view within the parent view.
    This partial view will be placed inside the layout.cshtml file, which is a shared (this is key) view that is under the wing of the home controller, just like the Home Views are -->
<!-- This code displays the culture/language dropdown.-->
<!-- Title of the dropdown-->
<div title="@Localizer["Request culture provider:"] @requestCulture?.Provider?.GetType().Name">
    <!-- another post method-->
    <!-- this form will call the setLanguage method under the AccountController.cs file. Even though this is a shared view, it's shared nature means the AccountController can still see it and act off of it.-->
    <form id="selectLanguage" asp-controller="Account" asp-action="SetLanguage" asp-route-returnUrl="@Context.Request.Path"
          method="post" class="form-horizontal" role="form">


        <!-- Select dropdown for the language selection -->
        <!-- asp-for indicates -->
        <a style="color:white"> @Localizer["Language"]</a>
        <select name="culture" asp-for="@requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems"></select>
        <button type="submit" class="btn btn-default btn-xs">Save</button>
        <!-- clicking on the save button will call the action setLanguage in the AccountController.-->
    </form>


</div>

部分异步调用

<!-- import the partial view for selecting languages _SelectLanguagePartial.cshtml -->
                @await Html.PartialAsync("_SelectLanguagePartial");

如果有人能阐明从这里到哪里去,我将不胜感激。我不想再为此扯头发了。我最后的努力是在 CookieOptions 中设置 isEssential = true 以覆盖 startup.cs

中定义的以下请求配置
services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => false;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

重要编辑: 我不知道如何或为什么,但我设法在我的 Web 应用程序的 separate 页面上进行了本地化,该页面在 after 后访问用户使用用户名和密码成功登录。如果页面发生变化,所选语言仍然存在,这正是我正在寻找的,即使我注销回登录页面也是如此。这很好,但我仍然无法从我的 Web 应用程序的登录页面更改语言或文化,其后端功能由 AccountController.cs 处理。任何人都知道是什么导致了这种奇怪的现象?

services.AddMvc()services.Configure<RequestLocalizationOptions> 的顺序很重要。确保包括本地化在内的所有内容都在 AddMvC.

之前 运行

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/troubleshoot-aspnet-core-localization?view=aspnetcore-3.1:

Localization middleware order The app may not localize because the localization middleware isn't ordered as expected. To resolve this issue, ensure that localization middleware is registered before MVC middleware. Otherwise, the localization middleware isn't applied

昨天,我在 ASP.NET Core 3.1 中构建了以下设置:

Startup.cs 配置 .AddMvc之前:

app.UseRequestLocalization();

我还认为您在 RequestLocalizationOptions 中遗漏了几行:

CultureInfo[] supportedCultures = new[]
{
    new CultureInfo("en-US"),
    new CultureInfo("ko-KR")
};
services.Configure<RequestLocalizationOptions>(options =>
{
    options.DefaultRequestCulture = new RequestCulture("en-US");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
    options.RequestCultureProviders = new List<IRequestCultureProvider>
    {
        new QueryStringRequestCultureProvider(),
        new CookieRequestCultureProvider()
    };
});

您没有指定侦听 cookie 的 CookieRequestCultureProvider。这应该是您的示例不起作用的第二个原因。

我发布这个问题已经有很长一段时间了,但我想指出,Citronas 关于上面 [AllowAnonymous] 数据注释的最终评论被证明是我正在寻找的解决方案。

只需将它放在 AccountController.cs 文件中的 SetLanguage 方法之上,一切都非常顺利。

我在 ASP.NET CORE 3.1

中遇到了同样的问题

解决方法很简单,就是没有地方写对。 只需修改您的 services.Configure<CookiePolicyOptions> 如下:

services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential 
            // cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            // requires using Microsoft.AspNetCore.Http;
            //options.MinimumSameSitePolicy = SameSiteMode.None;
            options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
        });

您无需登录即可使用 cookie。