最近在做.NET Framework 到 .NET 5.0 的升级,其中遇到了一个重要的问题就是跨域请求的问题。因为老项目中主要采用的是JSONP的方式实现跨域请求,因此为了保证对老系统的支持,我们仍需实现对JSONP的支持。但是在实际升级过程中,却发现现有关于.NET 5.0下JSONP实现的文章少之又少,少有的几篇文章,经尝试后发现仅能运用于.NET Core 3.1及之前的版本,而不再适用于.NET 5.0 框架。因此我在查阅官方文档以及不断尝试后,寻找出了一个方法,在此记录分享,也希望有大佬能给我提出意见建议或是更好的实现方法。
写在前面- 微软在.NET 5.0中更推荐采用 (CORS) 启用跨域请求 ASP.NET Core。如果不是必须采用JSONP,大家可以考虑改用 跨域资源共享(CORS) 来实现跨域。
- 对于.NET Core 3.1及之前的版本,或者是采用CORS方式的,可以直接参考文章:Asp.Net Core 中的跨域解决方案(Cors、jsonp改造、chrome配置)
本文参考官方文档ASP.NET Core Web API 中的自定义格式化程序实现
1、创建JSONP格式化程序类首先,我们需要先创建一个JSONP的格式化程序类JsonpMediaTypeFormatter,该类派生自TextOutputFormatter。
public class JsonpMediaTypeFormatter : TextOutputFormatter
{
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
throw new NotImplementedException();
}
}
2、在构造函数中指定有效的媒体类型和编码
我们需要在JsonpMediaTypeFormatter 的构造函数中指定有效的媒体类型和编码。
private static readonly MediaTypeHeaderValue JsonType = new MediaTypeHeaderValue("application/json");
private static readonly MediaTypeHeaderValue JsType = new MediaTypeHeaderValue("application/javascript");
private static readonly MediaTypeHeaderValue TextType = new MediaTypeHeaderValue("text/javascript");
public JsonpMediaTypeFormatter()
{
SupportedMediaTypes.Add(JsonType);
SupportedMediaTypes.Add(JsType);
SupportedMediaTypes.Add(TextType);
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
3、指定使用JSONP格式的条件
我们需要通过重写 CanWriteType 方法来指定何时启动JSONP格式对返回数据进行处理。通常,我们在采用GET请求,callback数非空时,实现对JSONP请求的响应。
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
if (context.HttpContext.Request.Method != HttpMethods.Get)
{
return false;
}
string callback = HttpUtility.UrlDecode((string)context.HttpContext.Request.Query["callback"]);
return !string.IsNullOrEmpty(callback);
}
4、改造数据结果,实现JSONP格式
最后,只需要重写WriteResponseBodyAsync方法,即可根据callback参数,返回对应的JSONP格式数据。
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
string callback = HttpUtility.UrlDecode((string)context.HttpContext.Request.Query["callback"]);
var buffer = new StringBuilder();
buffer.Append(callback);
buffer.Append('(');
buffer.Append(JsonSerializer.Serialize(context.Object));
buffer.Append(')');
await context.HttpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}
5、注册JSONP格式化程序类
这时候要想正常使用JSONP格式,还差最后一步。我们需要在ConfigureServices中注册我们刚创建的JsonpMediaTypeFormatter。
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.OutputFormatters.Insert(0, new JsonpMediaTypeFormatter());
});
}
6、最后
至此,我们的Web API就可以支持JSONP请求啦,最后附上完整的JsonpMediaTypeFormatter代码。
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
namespace Formatter
{
public class JsonpMediaTypeFormatter : TextOutputFormatter
{
private static readonly MediaTypeHeaderValue JsonType = new MediaTypeHeaderValue("application/json");
private static readonly MediaTypeHeaderValue JsType = new MediaTypeHeaderValue("application/javascript");
private static readonly MediaTypeHeaderValue TextType = new MediaTypeHeaderValue("text/javascript");
public JsonpMediaTypeFormatter()
{
SupportedMediaTypes.Add(JsonType);
SupportedMediaTypes.Add(JsType);
SupportedMediaTypes.Add(TextType);
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
if (context.HttpContext.Request.Method != HttpMethods.Get)
{
return false;
}
string callback = HttpUtility.UrlDecode((string)context.HttpContext.Request.Query["callback"]);
return !string.IsNullOrEmpty(callback);
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
string callback = HttpUtility.UrlDecode((string)context.HttpContext.Request.Query["callback"]);
var buffer = new StringBuilder();
buffer.Append(callback);
buffer.Append('(');
buffer.Append(JsonSerializer.Serialize(context.Object));
buffer.Append(')');
await context.HttpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}
}
}
附:失败的经历
在最初我是试图采用筛选器 ActionFilterAttribute ,通过重写
OnActionExecuted方法来实现对数据结果的调整。但在实际测试中发现,传入callback参数后会发生如下错误,故而放弃了采用该种方式实现JSONP的想法。
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id “0HM8UGOM59TCA”, Request id “0HM8UGOM59TCA:00000006”: An unhandled exception was thrown by the application.
System.InvalidOperationException: Headers are read-only, response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_ContentType(String value)
at Microsoft.AspNetCore.Mvc.Formatters.OutputFormatter.WriteResponseHeaders(OutputFormatterWriteContext context)
at Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter.WriteAsync(OutputFormatterWriteContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, Object value)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync(ActionContext context, ObjectResult result)
at Microsoft.AspNetCore.Mvc.ObjectResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultAsync(IActionResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextResultFilterAsyncTFilter,TFilterAsync
— End of stack trace from previous location —
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
— End of stack trace from previous location —
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)



