现象

在 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>你好,世界。</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));

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

依赖注入在 dotnet core 中实现与使用:5. 使用支持 Unicode 的 HtmlEncoder的更多相关文章

  1. 依赖注入在 dotnet core 中实现与使用:1 基本概念

    关于 Microsoft Extension: DependencyInjection 的介绍已经很多,但是多数偏重于实现原理和一些特定的实现场景.作为 dotnet core 的核心基石,这里准备全 ...

  2. 依赖注入在 dotnet core 中实现与使用:2 使用 Extensions DependencyInjection

    既然是依赖注入容器,必然会涉及到服务的注册,获取服务实例,管理作用域,服务注入这四个方面. 服务注册涉及如何将我们的定义的服务注册到容器中.这通常是实际开发中使用容器的第一步,而容器本身通常是由框架来 ...

  3. 依赖注入在 dotnet core 中实现与使用:4. 集成 Autofac

    本示例使用 .net core 5 rc-1 实现. 1. 添加 Nuget 包引用 使用 Autofac 当然要添加 Autofac 的 Nuget 包,主要涉及到两个: Autofac.Exten ...

  4. 依赖注入在 dotnet core 中实现与使用:3 使用 Lazy<T> 延迟实例化

    有些对象我们并不想一开始就实例化,由于性能或者功能的考虑,希望等到使用的时候再实例化.考虑存在一个类 A, 它使用了依赖的类 B,在 A 中,只有某些不常用到的方法会涉及调用 B 中的方法,多数情况下 ...

  5. Dotnet Core中使用AutoMapper

    官网:http://automapper.org/ 文档:https://automapper.readthedocs.io/en/latest/index.html GitHub:https://g ...

  6. 依赖注入[8]: .NET Core DI框架[服务消费]

    包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IServiceProvider对象.当需要消费某个服务实例的时候,我们只需要指定服务类型调用IServicePr ...

  7. 依赖注入[7]: .NET Core DI框架[服务注册]

    包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IServiceProvider对象.服务注册就是创建出现相应的ServiceDescriptor对象并将其添加到 ...

  8. 依赖注入[6]: .NET Core DI框架[编程体验]

    毫不夸张地说,整个ASP.NET Core框架是建立在一个依赖注入框架之上的,它在应用启动时构建请求处理管道过程中,以及利用该管道处理每个请求过程中使用到的服务对象均来源于DI容器.该DI容器不仅为A ...

  9. .NET CORE学习笔记系列(2)——依赖注入[7]: .NET Core DI框架[服务注册]

    原文https://www.cnblogs.com/artech/p/net-core-di-07.html 包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IS ...

  10. .NET CORE学习笔记系列(2)——依赖注入[6]: .NET Core DI框架[编程体验]

    原文https://www.cnblogs.com/artech/p/net-core-di-06.html 毫不夸张地说,整个ASP.NET Core框架是建立在一个依赖注入框架之上的,它在应用启动 ...

随机推荐

  1. Go语言中的位运算符

    位运算(bitwise operations)是计算机科学中非常基础且重要的运算类型,它直接操作二进制位.Go语言中提供了一组位运算符,用于执行位级别的操作. Go语言中的位运算符 按位与(& ...

  2. cpu proc sys文件系统下的含义

    proc文件系统(/proc/sys/kernel/) sched_child_runs_first /proc/sys/kernel/sched_child_runs_first是Linux内核中的 ...

  3. 《An Image Patch is a Wave: Phase-Aware Vision MLP》结构图+个人做的验证实验

    今天阅读了<An Image Patch is a Wave: Phase-Aware Vision MLP>这篇论文,根据代码绘制的它的结构图.如果有错误,还请指正. Wave_MLP_ ...

  4. 云原生周刊:Gateway API 1.0.0 发布 | 2023.11.6

    开源项目推荐 Kueue Kueue 是一套用于作业队列的 API 和控制器.它是作业级管理器,可决定何时允许作业启动(如创建 pod),何时停止作业(如删除活动 pod). Reloader 一个 ...

  5. 云原生周刊 | 使用 ChatGPT 协助解决 Prometheus 告警

    开源项目推荐 kubernetes-chatgpt-bot 这是一个适用于 Slack 的 ChatGPT 机器人,只要有监控告警发送到 Slack 频道中,你就可以通过机器人向 ChatGPT 咨询 ...

  6. 反射+特性实现 类和XML文档的序列化反序列化

    1.作用:将实体类的属性(字符串.日期.数值.布而.类)生成为xml文档中的结点,将xml文档的结点值或者属性值填充到实体类的属性值中2.思路:特性.反射.泛型:特性记录xml结点与实体属性的对应关系 ...

  7. ESP8266 + MQTT (platformio 开发环境)加用户名和密码

    ESP8266 + MQTT git 地址: https://gitee.com/zhudachangs/esp8266-mqtt.git (如果无法打开说明在审核) 引用库 include < ...

  8. ABC365(D,E)

    ABC365(D,E) D - AtCoder Janken 3 石头剪刀布,给出对手的出招,问在保证不败的情况下最多能赢多少回 记 \(f_i,{0/1/2}\) 表示第 \(i\) 局出石头/剪刀 ...

  9. 4-11.1 C++ 整型的隐式类型转换

    整型在运算时小类型会自动转换为大类型[保证精度] 右值给左值赋值时,会被转换为左值类型 int i; long long j; int res = i+j; //i转换为long long,i+j的类 ...

  10. 牛客多校H题题解

    链接:[https://ac.nowcoder.com/acm/contest/81597/H] 来源:牛客网 题目描述 Red stands at the coordinate \((0,0)\) ...