覆盖webapi odata链接的主机

时间:2016-02-25 13:17:07

标签: asp.net asp.net-web-api odata odata-v4

我正在使用WebAPI 2.2和Microsoft.AspNet.OData 5.7.0来创建支持分页的OData服务。

在生产环境中托管时,WebAPI位于未向外部公开的服务器上,因此OData响应中返回的各种链接(例如@odata.context@odata.nextLink指向内部IP地址,例如http://192.168.X.X/<AccountName>/api/...等。

我已经能够通过在每个ODataController方法中实现一些逻辑来修改Request.ODataProperties().NextLink,以用https://account-name.domain.com/api/...之类的外部URL替换内部URL,但这非常不方便,而且只是修复了NextLinks。

有没有办法在OData服务的配置时设置外部主机名?我看过一个属性Request.ODataProperties().Path并想知道是否可以在config.MapODataServiceRoute("odata", "odata", GetModel());调用或GetModel()实现中设置基本路径,例如使用ODataConventionModelBuilder?< / p>

更新:到目前为止,我提出的最佳解决方案是创建一个覆盖BaseODataController方法的Initialize,并检查{{1}然后像这样执行RequestUri重写:

Request.RequestUri.Host.StartsWith("beginning-of-known-internal-IP-address")

这完美地修复了所有控制器的var externalAddress = ConfigClient.Get().ExternalAddress; // e.g. https://account-name.domain.com var account = ConfigClient.Get().Id; // e.g. AccountName var uriToReplace = new Uri(new Uri("http://" + Request.RequestUri.Host), account); string originalUri = Request.RequestUri.AbsoluteUri; Request.RequestUri = new Uri(Request.RequestUri.AbsoluteUri.Replace(uriToReplace.AbsoluteUri, externalAddress)); string newUri = Request.RequestUri.AbsoluteUri; this.GetLogger().Info($"Request URI was rewritten from {originalUri} to {newUri}"); 网址,但出于某种原因,@odata.nextLink网址仍然获得了@odata.context部分(例如https://account-name.domain.com/AccountName/api/odata/ $元数据#ControllerName)所以他们仍然无法工作。

6 个答案:

答案 0 :(得分:4)

重写RequestUri足以影响@odata.nextLink值,因为code that computes the next link直接取决于RequestUri。其他@odata.xxx链接是通过UrlHelper计算的,以某种方式引用原始请求URI中的路径。 (因此,您在AccountName链接中看到@odata.context。我在代码中看到了这种行为,但我无法找到缓存的URI路径的来源。)

我们可以通过创建RequestUri类来动态重写OData链接来解决问题,而不是重写CustomUrlHelper。新的GetNextPageLink方法将处理@odata.nextLink重写,Link方法覆盖将处理所有其他重写。

public class CustomUrlHelper : System.Web.Http.Routing.UrlHelper
{
    public CustomUrlHelper(HttpRequestMessage request) : base(request)
    { }

    // Change these strings to suit your specific needs.
    private static readonly string ODataRouteName = "ODataRoute"; // Must be the same as used in api config
    private static readonly string TargetPrefix = "http://localhost:8080/somePathPrefix"; 
    private static readonly int TargetPrefixLength = TargetPrefix.Length;
    private static readonly string ReplacementPrefix = "http://www.contoso.com"; // Do not end with slash

    // Helper method.
    protected string ReplaceTargetPrefix(string link)
    {
        if (link.StartsWith(TargetPrefix))
        {
            if (link.Length == TargetPrefixLength)
            {
                link = ReplacementPrefix;
            }
            else if (link[TargetPrefixLength] == '/')
            {
                link = ReplacementPrefix + link.Substring(TargetPrefixLength);
            }
        }

        return link;
    }

    public override string Link(string routeName, IDictionary<string, object> routeValues)
    {
        var link = base.Link(routeName, routeValues);

        if (routeName == ODataRouteName)
        {
            link = this.ReplaceTargetPrefix(link);
        }

        return link;
    }

    public Uri GetNextPageLink(int pageSize)
    {
        return new Uri(this.ReplaceTargetPrefix(this.Request.GetNextPageLink(pageSize).ToString()));
    }
}

连接基本控制器类的CustomUrlHelper方法中的Initialize

public abstract class BaseODataController : ODataController
{
    protected abstract int DefaultPageSize { get; }

    protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
    {
        base.Initialize(controllerContext);

        var helper = new CustomUrlHelper(controllerContext.Request);
        controllerContext.RequestContext.Url = helper;
        controllerContext.Request.ODataProperties().NextLink = helper.GetNextPageLink(this.DefaultPageSize);
    }

请注意,在给定的控制器类中,页面大小对于所有操作都是相同的。您可以通过将ODataProperties().NextLink的分配移动到特定操作方法的主体来解决此限制,如下所示:

var helper = this.RequestContext.Url as CustomUrlHelper;
this.Request.ODataProperties().NextLink = helper.GetNextPageLink(otherPageSize);

答案 1 :(得分:2)

还有另一种解决方案,但它会覆盖整个上下文的url。 我想建议的是:

  1. 创建owin中间件并覆盖
  2. 中的Host和Scheme属性
  3. 将中间件注册为第一个
  4. 以下是中间件

    的示例
    public class RewriteUrlMiddleware : OwinMiddleware
    {
        public RewriteUrlMiddleware(OwinMiddleware next)
            : base(next)
        {
        }
    
        public override async Task Invoke(IOwinContext context)
        {
            context.Request.Host = new HostString(Settings.Default.ProxyHost);
            context.Request.Scheme = Settings.Default.ProxyScheme;
            await Next.Invoke(context);
        }
    }
    

    ProxyHost是您想拥有的主机。示例:test.com

    ProxyScheme是您想要的方案:示例:https

    中间件注册示例

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Use(typeof(RewriteUrlMiddleware));
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);
            app.UseWebApi(config);
        }
    }
    

答案 2 :(得分:2)

几年后,我发现使用ASP.NET Core在服务中应用它的最简单方法是创建一个伪装主机名的过滤器。 (public class MasqueradeHostFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { var appConfig = context.HttpContext.RequestServices.GetService<AppConfig>(); if (!string.IsNullOrEmpty(appConfig?.MasqueradeHost)) context.HttpContext.Request.Host = new HostString(appConfig.MasqueradeHost); } } 是一个自定义配置类,其中包含主机名,等等。)

[MasqueradeHostFilter]
public class AppODataController : ODataController
{
}

将过滤器应用于控制器基类。

{ "@odata.context":"https://app.example.com/odata/$metadata" }

结果是格式良好的输出:

series: [{
                    name: 'test',
                    data: testData,
                    states: {
                        hover: {
                            color: '#95CEFF',
                        }
                    }
                }]

只要两美分。

答案 3 :(得分:1)

lencharest的答案很有希望,但我发现他的方法有所改进。我没有使用UrlHelper,而是创建了一个从System.Net.Http.DelegatingHandler派生的类。此类(首先)插入到消息处理管道中,因此在更改传入的HttpRequestMessage方面存在裂痕。这是对上述解决方案的改进,因为除了更改控制器特定的URL(如UrlHelper所做的,例如https://data.contoso.com/odata/MyController)之外,它还更改了在OData服务中显示为xml:base的URL文档(例如https://data.contoso.com/odata)。

我的特定应用是在代理服务器后面托管OData服务,我希望服务器提供的所有URL是外部可见的URL,而不是内部可见的URL。而且,我不想为此依赖注释;我希望它是全自动的。

消息处理程序如下:

    public class BehindProxyMessageHandler : DelegatingHandler
    {
        protected async override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var builder = new UriBuilder(request.RequestUri);
            var visibleHost = builder.Host;
            var visibleScheme = builder.Scheme;
            var visiblePort = builder.Port;

            if (request.Headers.Contains("X-Forwarded-Host"))
            {
                string[] forwardedHosts = request.Headers.GetValues("X-Forwarded-Host").First().Split(new char[] { ',' });
                visibleHost = forwardedHosts[0].Trim();
            }

            if (request.Headers.Contains("X-Forwarded-Proto"))
            {
                visibleScheme = request.Headers.GetValues("X-Forwarded-Proto").First();
            }

            if (request.Headers.Contains("X-Forwarded-Port"))
            {
                try
                {
                    visiblePort = int.Parse(request.Headers.GetValues("X-Forwarded-Port").First());
                }
                catch (Exception)
                { }
            }

            builder.Host = visibleHost;
            builder.Scheme = visibleScheme;
            builder.Port = visiblePort;

            request.RequestUri = builder.Uri;
            var response = await base.SendAsync(request, cancellationToken);
            return response;
        }
    }

您可以在WebApiConfig.cs中连接处理程序:

    config.Routes.MapODataServiceRoute(
        routeName: "odata",
        routePrefix: "odata",
        model: builder.GetEdmModel(),
        pathHandler: new DefaultODataPathHandler(),
        routingConventions: ODataRoutingConventions.CreateDefault()
    );
    config.MessageHandlers.Insert(0, new BehindProxyMessageHandler());

答案 4 :(得分:0)

使用system.web.odata 6.0.0.0。

过早设置NextLink属性是有问题的。然后每个回复都会包含一个nextLink。最后一页当然应该没有这样的装饰。

http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793048说:

  

有效负载中存在的URL(无论是请求还是响应)可能是   表示为相对URL。

我希望其中一种方法是覆盖EnableQueryAttribute:

public class myEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        var result = base.ApplyQuery(queryable, queryOptions);
        var nextlink = queryOptions.Request.ODataProperties().NextLink;
        if (nextlink != null)
            queryOptions.Request.ODataProperties().NextLink = queryOptions.Request.RequestUri.MakeRelativeUri(nextlink);
        return result;
    }
}

ApplyQuery()是检测到“溢出”的地方。它基本上会询问pagesize+1行,如果结果集包含的行数超过NextLink,则会设置pagesize

此时,将NextLink重写为相对网址相对容易。

缺点是现在必须使用新的myEnableQuery属性装饰每个odata方法:

[myEnableQuery]
public async Task<IHttpActionResult> Get(ODataQueryOptions<TElement> options)
{
  ...
}

和其他地方嵌入的其他网址仍然存在问题。 odata.context仍然是一个问题。我想避免使用请求URL,因为我无法看到它是如何随着时间的推移而维护的。

答案 5 :(得分:-1)

您的问题归结为从服务本身控制服务根URI。我的第一个想法是在用于序列化响应的媒体类型格式化程序上寻找一个钩子。 ODataMediaTypeFormatter.MessageWriterSettings.PayloadBaseUriODataMediaTypeFormatter.MessageWriterSettings.ODataUri.ServiceRoot都是可设置的属性,可以提供解决方案。不幸的是,ODataMediaTypeFormatter每次调用都会IODataPathHandler.Link resets这些属性。

解决方法并不明显,但如果你仔细阅读源代码,你最终会调用public class CustomPathHandler : DefaultODataPathHandler { private const string ServiceRoot = "http://example.com/"; public override string Link(ODataPath path) { return ServiceRoot + base.Link(path); } } 。路径处理程序是OData扩展点,因此您可以创建一个自定义路径处理程序,该处理程序始终返回以您希望的服务根开头的绝对URI。

// config is an instance of HttpConfiguration
config.MapODataServiceRoute(
    routeName: "ODataRoute",
    routePrefix: null,
    model: builder.GetEdmModel(),
    pathHandler: new CustomPathHandler(),
    routingConventions: ODataRoutingConventions.CreateDefault()
);

然后在服务配置期间注册该路径处理程序。

<=
相关问题