【问题标题】:Can't generate a Response cookie for localization using ASP.NET CORE 2.2无法使用 ASP.NET CORE 2.2 为本地化生成响应 cookie
【发布时间】:2020-01-15 11:59:10
【问题描述】:

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

我尝试刷新 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 应用程序的登录页面更改语言或文化,其后端功能由 AccountController.cs 处理。有人对导致这种奇怪现象的原因有任何想法吗?

【问题讨论】:

    标签: asp.net-core cookies localization


    【解决方案1】:

    我在使用 ASP.NET CORE 3.1 时遇到了同样的问题

    解决方案很简单,但写得不好。 只需将您的services.Configure&lt;CookiePolicyOptions&gt; 修改如下:

    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 工作。

    【讨论】:

    • 谢谢。从 2.2 升级后,在 3.1 中为我工作并且 cookie 不起作用:D
    • 这很适合我。我一直在寻找这个。谢谢。非常感谢!
    【解决方案2】:

    services.AddMvc()services.Configure&lt;RequestLocalizationOptions&gt; 的顺序很重要。确保包括本地化在内的所有内容都在 AddMvC 之前运行。

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

    本地化中间件顺序 应用程序可能无法本地化,因为本地化中间件未按预期排序。 要解决此问题,请确保在 MVC 中间件之前注册本地化中间件。否则,不应用本地化中间件

    昨天,我在 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。这应该是您的示例不起作用的第二个原因。

    【讨论】:

    • 感谢您的回复。我添加了完整的 Startup.cs 文件并进行了编辑,以便您可以看到完整的图片。据我所知,app.UseRequestLocalizationaddMvc.() 出现之前就被提及。我现在尝试添加RequestCultureProvidersAccountController.cs 文件呢?看起来怎么样?
    • 处理POST的方法是SetLanguage(string culture, string returnURL)
    • RequestCultureProviders 建议无效。在我的主 Web 应用程序上仍然看不到响应 cookie,但虚拟的 cookie 工作得很好。
    • 我在原帖底部做了一个重要的修改。不知何故,我在登录后能够使本地化服务正常工作,但我希望我的用户也能够从登录页面更改语言,但这仍然无法正常工作。
    • 也许您需要将[AllowAnonymous] 添加到您的登录方法中?
    【解决方案3】:

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

    只需将它放在 AccountController.cs 文件中的 SetLanguage 方法之上,一切都像魅力一样工作。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-07
      • 2020-03-16
      • 1970-01-01
      • 1970-01-01
      • 2017-01-06
      相关资源
      最近更新 更多