zoukankan      html  css  js  c++  java
  • 依赖注入在 dotnet core 中实现与使用:5. 使用支持 Unicode 的 HtmlEncoder

    现象

    在 ASP.NET Core MVC 中,当在页面中传递了一个包含中文字符串到页面的时候,页面的显示是正常的,但是如果查看页面源码,却看不到中文,变成了一串编码之后的内容。
    例如,在页面中直接定义一个含有中文内容的字符串,然后在页面中显示出来。

    @{
        ViewData["Title"] = "Home Page";
         string world = "世界";
    }
    
    <div class="text-center">
        <h1 class="display-4">Welcome</h1>
        <p>你好,@world。</p>
    </div>
    

    运行之后,可以看到页面是正常的

    但是在查看页面源码的时候,中文不见了。

    <p>你好,&#x4E16;&#x754C;。</p>
    

    原因就是字符串的内容是通过代码进行编码之后输出的。

    分析

    在 asp.net core 中,基于防范 xss 攻击的安全考虑,默认将所有非基本字符(U+0000..U+007F)的字符进行编码。因此基本除了英文字母那一部分,其他的全被编码了。

    这个控制来源于 HtmlEncoder 中的 UnicodeRange 被设置成 UnicodeRanges.BasicLatin。

    编码使用了 HtmlEncoder 这个类。它位于项目 System.Text.Encodings.Web at GitHub 中,默认它使用了 DefaultHtmlEncoder 进行编码,但是它基于拉丁字符进行编码。下面是源代码片段:

        internal sealed class DefaultHtmlEncoder : HtmlEncoder
        {
            private readonly AllowedCharactersBitmap _allowedCharacters;
            internal static readonly DefaultHtmlEncoder Singleton = new DefaultHtmlEncoder(new TextEncoderSettings(UnicodeRanges.BasicLatin));
    

    以及:

        public abstract class HtmlEncoder : TextEncoder
        {
            /// <summary>
            /// Returns a default built-in instance of <see cref="HtmlEncoder"/>.
            /// </summary>
            public static HtmlEncoder Default
            {
                get { return DefaultHtmlEncoder.Singleton; }
            }
    

    点击这里 查看完全的源代码。

    解决方案

    配置将 UnicodeRange 范围放宽。我们可以通过将编码器替换为支持中文的 Unicode 编码器来解决它。

    在 ASP.NET Core 中,各种服务是通过依赖注入来提供服务的。所以,我们可以通过调整容器中注册的服务来解决这个问题。

    源代码中其实注入了一个 HtmlEncoder 来处理编码问题。

    public DefaultHtmlGenerator(
          IAntiforgery antiforgery,
          IOptions<MvcViewOptions> optionsAccessor,
          IModelMetadataProvider metadataProvider,
          IUrlHelperFactory urlHelperFactory,
          HtmlEncoder htmlEncoder,
          ClientValidatorCache clientValidatorCache)
    {
    }
    

    源码见:https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs

    ASP.NET Core 本身已经注册了 HtmlEncoder 的服务,并且提供了使用 WebEncoderOptions 的方式进行配置。
    代码如下所示:

            public static IServiceCollection AddWebEncoders(this IServiceCollection services)
            {
                if (services == null)
                {
                    throw new ArgumentNullException(nameof(services));
                }
    
                services.AddOptions();
    
                // Register the default encoders
                // We want to call the 'Default' property getters lazily since they perform static caching
                services.TryAddSingleton(
                    CreateFactory(() => HtmlEncoder.Default, settings => HtmlEncoder.Create(settings)));
                services.TryAddSingleton(
                    CreateFactory(() => JavaScriptEncoder.Default, settings => JavaScriptEncoder.Create(settings)));
                services.TryAddSingleton(
                    CreateFactory(() => UrlEncoder.Default, settings => UrlEncoder.Create(settings)));
    
                return services;
            }
    ...
            private static Func<IServiceProvider, TService> CreateFactory<TService>(
                Func<TService> defaultFactory,
                Func<TextEncoderSettings, TService> customSettingsFactory)
            {
                return serviceProvider =>
                {
                    var settings = serviceProvider
                        ?.GetService<IOptions<WebEncoderOptions>>()
                        ?.Value
                        ?.TextEncoderSettings;
                    return (settings != null) ? customSettingsFactory(settings) : defaultFactory();
                };
            }
    

    这里使用了一个工厂来创建 HtmlEncoder 等 3 个对象。该工厂在创建过程中还会尝试寻找 WebEncoderOptions 配置对象,如果有的话,会使用它作为配置参数来创建这 3 个对象,否则使用默认方式。

    另外还提供了一个可以注册 WebEncoderOptions 选项的扩展方法,提供使用 Options 模式的支持。通过它可以配置当前使用的编码范围。

    public static IServiceCollection AddWebEncoders(this IServiceCollection services, Action<WebEncoderOptions> setupAction)
            {
                if (services == null)
                {
                    throw new ArgumentNullException(nameof(services));
                }
    
                if (setupAction == null)
                {
                    throw new ArgumentNullException(nameof(setupAction));
                }
    
                services.AddWebEncoders();
                services.Configure(setupAction);
    
                return services;
            }
    

    源码见:https://github.com/dotnet/aspnetcore/blob/master/src/WebEncoders/src/EncoderServiceCollectionExtensions.cs

    所以,有两个方案可以选择,一个是直接从 HtmelEncoder 入手,提供新的 HtmlEncoder 实现,更好的方式是通过 Options 模式,在创建的时候,提供适当的参数。

    有多种方式可以考虑,这里列出 5 种方式。

    第一种方式是直接再注册一个同类型的服务,由于对同一个类型注册多个服务的话,在注入单个服务实例的时候,会采用最后注册的服务,所以,我们可以再注册一个 HtmlEncoder 类型的服务,就可以解决这个问题。
    代码如下:

    services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
    

    这种方式有个缺点,多创建一个对象实例。

    第二种方式是直接替换掉原来的 HtmlEncoder 服务,可以使用 ServiceCollection 的扩展方法 Replace() 来实现,它接受一个 ServiceDescriptor 类型的参数进行替换。这样更彻底一点。

    var descriptor =
        new ServiceDescriptor(
            typeof(HtmlEncoder),
            HtmlEncoder.Create(UnicodeRanges.All));
    services.Replace(descriptor);
    

    这时候,只有一个扩展之后的 HtmlEncoder 在使用。此时没有多余的 HtmlEncoder 对象。
    需要注意的是,Replace() 扩展方法位于命名空间 System.Text.Encodings.Web 下,而 UnicodeRanges 位于 System.Text.Unicode 下,记得添加两个命名空间的引用。

    using System.Text.Encodings.Web;
    using System.Text.Unicode;
    

    第三种,既然提供了可以通过 Options 模式配置,还可以基于 Options 模式来处理。它使用 Configure 方法来进行,它通过 Action 来提供配置 WebEncoderOptions 对象。这样 3 种对象都可以直接使用,该方法定义如下:

    public static IServiceCollection Configure<TOptions>(
          this IServiceCollection services, 
          Action<TOptions> configureOptions) where TOptions : class;
    

    实现如下:

    services.Configure<Microsoft.Extensions.WebEncoders.WebEncoderOptions>(
        options =>
            options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All));
    

    这样在创建 HtmlEncoder 等 3 个对象的时候,将使用该配置对象。

    第四种方式,还是基于 Options 模式。是在 Configure() 方法之后进行配置。见:

    services.PostConfigure<Microsoft.Extensions.WebEncoders.WebEncoderOptions>(
        options =>
            options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All));
    

    效果与 #3 是相同的。但是 PostConfigure() 会保证在 Configure() 方法之后执行,比第 3 种更好。

    最后但是最好的方式,根据源码可以看到,还可以使用第 5 种方式,系统已经提供了扩展方法 AddWebEncoders。其实与 #3 是一样的。

    services.AddWebEncoders(options => options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All));
    

    这种方式更好一些。更加语义化。

  • 相关阅读:
    Ubuntu 12.04 root用户登录设置
    E: 无法获得锁 /var/lib/dpkg/lock open???
    每日英语:HardWired To Hate Exercise?
    每日英语:How to say No to other people
    每日英语:Family Inc.
    每日英语:An Unhappy Middle in the Middle Kingdom
    每日英语:How Many People Really Use Sina Weibo
    每日英语:The Deeply Odd Lives of Chinese Bureaucrats
    每日英语:The Tyranny Of The Queen Bee
    每日英语:Economist: China Plenty Creative, Just Not in Right Ways
  • 原文地址:https://www.cnblogs.com/haogj/p/13785680.html
Copyright © 2011-2022 走看看