冠军

导航

依赖注入在 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));

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

posted on 2021-09-03 14:12  冠军  阅读(406)  评论(0编辑  收藏  举报