记一次Swashbuckle.AspNetCore.Swagger接口文档本身跨域处理的问题排查

原创
2022/07/21 14:59
阅读数 1.5K

Swagger是一套非常方便的接口导出工具,在日常开发中使用非常广泛。 但是有时候在使用的时候也会发现有一些想不通的问题,比如:团队开发中需要将swagger.json作为自动导入的接口时,引起的跨域问题。 场景是这样的: 在Startup.cs里面设置跨域方案:

 services.AddCors(c =>
            {
                c.AddPolicy("myCors", policy =>
                {
                    policy
                    // 示例说明
                    .WithOrigins("ur1", "url2", "...")
                    .AllowAnyHeader()
                    .AllowAnyMethod();
                });
            });

并应用该方案

app.UseCors("myCors");

然后我们使用Swagger生成接口文档、并输出swagger.json文件。

 app.UseSwaggerUI(c => c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"Test.API {version}"));

当我们直接点击这个json文件时可以正常访问,但是在前端js访问的时候就会产生跨域问题。

首先的疑问,我都配了跨域方案,为何访问这个不生效?

于是,我立马去翻AspNetCore的源码,找到CorsMiddleware中间件,看到Invoke方法内容

 /// <inheritdoc />
    public Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider)
    {
        // CORS policy resolution rules:
        //
        // 1. If there is an endpoint with IDisableCorsAttribute then CORS is not run
        // 2. If there is an endpoint with ICorsPolicyMetadata then use its policy or if
        //    there is an endpoint with IEnableCorsAttribute that has a policy name then
        //    fetch policy by name, prioritizing it above policy on middleware
        // 3. If there is no policy on middleware then use name on middleware
        var endpoint = context.GetEndpoint();

        if (endpoint != null)
        {
            // EndpointRoutingMiddleware uses this flag to check if the CORS middleware processed CORS metadata on the endpoint.
            // The CORS middleware can only make this claim if it observes an actual endpoint.
            context.Items[CorsMiddlewareWithEndpointInvokedKey] = CorsMiddlewareWithEndpointInvokedValue;
        }

        if (!context.Request.Headers.ContainsKey(CorsConstants.Origin))
        {
            return _next(context);
        }

        // Get the most significant CORS metadata for the endpoint
        // For backwards compatibility reasons this is then downcast to Enable/Disable metadata
        var corsMetadata = endpoint?.Metadata.GetMetadata<ICorsMetadata>();

        if (corsMetadata is IDisableCorsAttribute)
        {

看注释意思就是说,无跨域方案或未应用方案时跨域不生效。这里只有当endpoint不为空时下面才会处理跨域方案逻辑。

接着我们再先来找一下Swagger里关于接口文档输出的逻辑,同样找到了一个关于Http拦截的中间件SwaggerMiddleware。

看下面的逻辑,就是匹配到swagger.json访问后,直接使用Response.Write写入输出流。

        public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
        {
            if (!RequestingSwaggerDocument(httpContext.Request, out string documentName))
            {
                await _next(httpContext);
                return;
            }

            try
            {
                var basePath = httpContext.Request.PathBase.HasValue
                    ? httpContext.Request.PathBase.Value
                    : null;

                var swagger = swaggerProvider switch
                {
                    IAsyncSwaggerProvider asyncSwaggerProvider => await asyncSwaggerProvider.GetSwaggerAsync(
                        documentName: documentName,
                        host: null,
                        basePath: basePath),
                    _ => swaggerProvider.GetSwagger(
                        documentName: documentName,
                        host: null,
                        basePath: basePath)
                };

                // One last opportunity to modify the Swagger Document - this time with request context
                foreach (var filter in _options.PreSerializeFilters)
                {
                    filter(swagger, httpContext.Request);
                }

                if (Path.GetExtension(httpContext.Request.Path.Value) == ".yaml")
                {
                    await RespondWithSwaggerYaml(httpContext.Response, swagger);
                }
                else
                {
                    await RespondWithSwaggerJson(httpContext.Response, swagger);
                }
            }
            catch (UnknownSwaggerDocument)
            {
                RespondWithNotFound(httpContext.Response);
            }
        }
 private async Task RespondWithSwaggerJson(HttpResponse response, OpenApiDocument swagger)
        {
            response.StatusCode = 200;
            response.ContentType = "application/json;charset=utf-8";

            using (var textWriter = new StringWriter(CultureInfo.InvariantCulture))
            {
                var jsonWriter = new OpenApiJsonWriter(textWriter);
                if (_options.SerializeAsV2) swagger.SerializeAsV2(jsonWriter); else swagger.SerializeAsV3(jsonWriter);

                await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false));
            }
        }

我们看到有PreSerializeFilters过滤器,有对于Request的参数传递,而Request对象本身也携带了HttpContext上下文,那应该也可以在这里添加跨域的请求头:

                app.UseSwagger(c =>
                {
                    c.PreSerializeFilters.Add((swagger, httpreq) =>
                    {
                        httpreq.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
                        httpreq.HttpContext.Response.Headers.Add("Access-Control-Allow-Methods", httpreq.Method);
                    });
                });

经过验证,这样就可以解决问题了。

但是,这样写,工程里面就又多出了一处独立的跨域方案设置,能不能使用已配置的跨域方案呢?

其实现在问题已经很明显了,就是UseCors 与 UseSwagger 里面都用了同样的Http拦截,当请求过来后都是在一个for循环里被执行。不同的是,UseSwagger在拦截到有效请求后直接Response.Write,这会导至之后的Response.Header设置无效。

所以,说到这里,将注册中间件的顺序调整正确就可以解决问题,验证一下:

            app.UseCors("myCors");
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                //....
            });

测试通过,什么都不用改变,就改变一下应用顺序!

最后

致这该死的顺序以及这个毫无说明的 response.Write!!!。

 

 

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部