Czy można utworzyć trasę ASP.NET MVC na podstawie subdomeny?

235

Czy możliwe jest posiadanie trasy ASP.NET MVC, która używa informacji z subdomeny w celu ustalenia jej trasy? Na przykład:

  • użytkownik1 .domena.com trafia w jedno miejsce
  • użytkownik2 .domena.com przechodzi na inny?

Czy mogę to zrobić, aby oba z nich przechodziły do ​​tego samego kontrolera / akcji z usernameparametrem?

Dan Esparza
źródło
Zaimplementowałem podobne rozwiązanie dla aplikacji z wieloma dzierżawcami, ale używając abstrakcyjnego bazowego kontrolera zamiast niestandardowej klasy Route. Mój wpis na blogu jest tutaj .
Luke Sampson,
6
Pamiętaj, aby wziąć pod uwagę to podejście: http://blog.tonywilliams.me.uk/asp-net-mvc-2-routing-subdomains-to-areas Stwierdziłem, że lepiej jest wprowadzić do mojej aplikacji obsługę wielu aplikacji niż inne odpowiedzi , ponieważ obszary MVC to dobry sposób na wprowadzenie kontrolerów i widoków specyficznych dla najemcy w zorganizowany sposób.
trebormf
2
@trebormf - Myślę, że powinieneś dodać to jako odpowiedź, to właśnie wykorzystałem jako podstawę mojego rozwiązania.
Shagglez
@Shagglez - Dzięki. To była odpowiedź, ale moderator przekształcił ją w komentarz z powodów, których nie rozumiem.
trebormf
5
Tony był jak zepsuty. Oto jeden, który zadziałał dla mnie: blog.tonywilliams.me.uk/…
Ronnie Overby

Odpowiedzi:

168

Możesz to zrobić, tworząc nową trasę i dodając ją do kolekcji tras w RegisterRoutes w pliku global.asax. Poniżej znajduje się bardzo prosty przykład niestandardowej trasy:

public class ExampleRoute : RouteBase
{

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var url = httpContext.Request.Headers["HOST"];
        var index = url.IndexOf(".");

        if (index < 0)
            return null;

        var subDomain = url.Substring(0, index);

        if (subDomain == "user1")
        {
            var routeData = new RouteData(this, new MvcRouteHandler());
            routeData.Values.Add("controller", "User1"); //Goes to the User1Controller class
            routeData.Values.Add("action", "Index"); //Goes to the Index action on the User1Controller

            return routeData;
        }

        if (subDomain == "user2")
        {
            var routeData = new RouteData(this, new MvcRouteHandler());
            routeData.Values.Add("controller", "User2"); //Goes to the User2Controller class
            routeData.Values.Add("action", "Index"); //Goes to the Index action on the User2Controller

            return routeData;
        }

        return null;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        //Implement your formating Url formating here
        return null;
    }
}
Jon Cahill
źródło
1
Dzięki za szczegółową próbkę, ale nie śledzę, jak wykonać .Add z Global.asax.
justSteve
4
Nazwałem trasę SubdomainRoute i dodałem ją jako pierwszą trasę w następujący sposób: route.Add (new SubdomainRoute ());
Jeff Handley
6
Czy takie podejście wymaga twardego zakodowania listy możliwych subdomen?
Maxim V. Pavlov
2
Nie, możesz dodać pole bazy danych o nazwie coś w rodzaju „subdomeny”, która będzie tym, czego oczekujesz od subdomeny dla konkretnego użytkownika lub cokolwiek innego, a następnie po prostu sprawdź subdomenę.
Ryan Hayes
1
Czy ktoś mógłby polecić wersję internetową tego formularza?
MatthewT
52

Aby przechwycić poddomenę, zachowując standardowe funkcje routingu MVC5 , użyj następującej SubdomainRouteklasy pochodnej Route.

Dodatkowo SubdomainRouteumożliwia opcjonalne określenie subdomeny jako parametru zapytania , tworzenia sub.example.com/foo/bari example.com/foo/bar?subdomain=subrównoważności. Umożliwia to testowanie przed skonfigurowaniem poddomen DNS. Parametr zapytania (gdy jest używany) jest propagowany przez nowe łącza generowane przez Url.Actionitp.

Parametr zapytania umożliwia także lokalne debugowanie w Visual Studio 2013 bez konieczności konfigurowania za pomocą netsh lub uruchamiania jako Administrator . Domyślnie IIS Express łączy się z localhost tylko wtedy, gdy nie jest podniesiony; nie będzie wiązać się z synonimicznymi nazwami hostów, takimi jak sub.localtest.me .

class SubdomainRoute : Route
{
    public SubdomainRoute(string url) : base(url, new MvcRouteHandler()) {}

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var routeData = base.GetRouteData(httpContext);
        if (routeData == null) return null; // Only look at the subdomain if this route matches in the first place.
        string subdomain = httpContext.Request.Params["subdomain"]; // A subdomain specified as a query parameter takes precedence over the hostname.
        if (subdomain == null) {
            string host = httpContext.Request.Headers["Host"];
            int index = host.IndexOf('.');
            if (index >= 0)
                subdomain = host.Substring(0, index);
        }
        if (subdomain != null)
            routeData.Values["subdomain"] = subdomain;
        return routeData;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
        if (subdomainParam != null)
            values["subdomain"] = subdomainParam;
        return base.GetVirtualPath(requestContext, values);
    }
}

Dla wygody wywołaj następującą MapSubdomainRoutemetodę ze swojej RegisterRoutesmetody, tak jak zwykły stary MapRoute:

static void MapSubdomainRoute(this RouteCollection routes, string name, string url, object defaults = null, object constraints = null)
{
    routes.Add(name, new SubdomainRoute(url) {
        Defaults = new RouteValueDictionary(defaults),
        Constraints = new RouteValueDictionary(constraints),
        DataTokens = new RouteValueDictionary()
    });
}

Wreszcie, aby wygodnie uzyskać dostęp do subdomeny (z prawdziwej subdomeny lub parametru zapytania), pomocne jest utworzenie podstawowej klasy kontrolera z tą Subdomainwłaściwością:

protected string Subdomain
{
    get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
Edward Brey
źródło
1
Zaktualizowałem kod, aby poddomena była zawsze dostępna jako wartość trasy. Upraszcza to dostęp do subdomeny.
Edward Brey,
Lubię to. Bardzo proste i więcej niż wystarczające dla mojego projektu.
SoonDead,
To świetna odpowiedź. Czy istnieje sposób, aby działało to z atrybutami trasy? Staram się, aby działało to w przypadku ścieżek takich jak „subdomain.domain.com/portal/register”, a użycie atrybutów ułatwiłoby to.
perfect_element
@perfect_element - Trasy atrybutów nie są rozszerzalne, podobnie jak trasy oparte na konwencjach. Jedynym sposobem na zrobienie czegoś takiego byłoby zbudowanie własnego systemu routingu atrybutów.
NightOwl888,
23

To nie jest moja praca, ale musiałem dodać ją do tej odpowiedzi.

Oto świetne rozwiązanie tego problemu. Maartin Balliauw napisał kod, który tworzy klasę DomainRoute, której można używać bardzo podobnie do normalnego routingu.

http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx

Przykładowe użycie wyglądałoby tak ...

routes.Add("DomainRoute", new DomainRoute( 
    "{customer}.example.com", // Domain with parameters 
    "{action}/{id}",    // URL with parameters 
    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults 
))

;

Jim Blake
źródło
5
Wystąpił problem z tym rozwiązaniem. Powiedzmy, że chcesz obsługiwać poddomeny jako różnych użytkowników: trasy.Add („SD”, new DomainRoute („user} .localhost”, „”, new {controller = "Home", action = "IndexForUser", user = "u1) „})); Buforuje również stronę główną. Wynika to z wygenerowanego wyrażenia regularnego. Aby to naprawić, możesz wykonać kopię metody CreateRegex w DomainRoute.cs, nazwać ją CreateDomainRegex, zmienić * w tym wierszu na +: source = source.Replace ("}", @ "> ([a- zA-Z0-9 _] *)) "); i użyj tej nowej metody regxowania domeny w metodzie GetRouteData: domainRegex = CreateDomainRegex (Domain);
Gorkem Pacaci
Nie wiem, dlaczego nie mogę uruchomić tego kodu ... Po prostu SERVER NOT FOUNDpojawia się błąd ... oznacza, że ​​kod nie działa dla mnie ... ustawiasz inną konfigurację czy coś takiego ?!
Dr TJ
Utworzyłem Gist mojej wersji tego gist.github.com/IDisposable/77f11c6f7693f9d181bb
IDisposable
1
@IDisposable co to jest MvcApplication.DnsSuffix?
HaBo
Po prostu ujawniamy podstawową domenę DNS w pliku web.config ... typową wartością byłoby .example.org
IDisposable
4

Aby przechwycić poddomenę podczas korzystania z interfejsu API sieci Web , należy przesłonić Selektor akcji i wprowadzić subdomainparametr zapytania. Następnie użyj parametru zapytania poddomeny w swoich działaniach kontrolerów:

public string Get(string id, string subdomain)

Takie podejście sprawia, że ​​debugowanie jest wygodne, ponieważ można ręcznie określić parametr zapytania, używając lokalnego hosta zamiast rzeczywistej nazwy hosta (szczegółowe informacje znajdują się w standardowej odpowiedzi routingu MVC5 ). Oto kod Selektora akcji:

class SubdomainActionSelector : IHttpActionSelector
{
    private readonly IHttpActionSelector defaultSelector;

    public SubdomainActionSelector(IHttpActionSelector defaultSelector)
    {
        this.defaultSelector = defaultSelector;
    }

    public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
    {
        return defaultSelector.GetActionMapping(controllerDescriptor);
    }

    public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
    {
        var routeValues = controllerContext.Request.GetRouteData().Values;
        if (!routeValues.ContainsKey("subdomain")) {
            string host = controllerContext.Request.Headers.Host;
            int index = host.IndexOf('.');
            if (index >= 0)
                controllerContext.Request.GetRouteData().Values.Add("subdomain", host.Substring(0, index));
        }
        return defaultSelector.SelectAction(controllerContext);
    }
}

Zastąp domyślny Selektor akcji, dodając go do WebApiConfig.Register:

config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Edward Brey
źródło
Czy ktoś ma problemy z tym, że dane trasy nie pojawiają się na kontrolerze interfejsu API sieci Web i sprawdza Request.GetRouteData wewnątrz kontrolera, nie wyświetla żadnych wartości?
Alan Macdonald,
3

Tak, ale musisz utworzyć własny moduł obsługi trasy.

Zazwyczaj trasa nie zna domeny, ponieważ aplikację można wdrożyć w dowolnej domenie, a trasa nie miałaby znaczenia w ten czy inny sposób. Ale w twoim przypadku chcesz oprzeć kontroler i działanie poza domeną, więc będziesz musiał utworzyć niestandardową trasę, która zna domenę.

Nick Berardi
źródło
3

Stworzyłem bibliotekę dla routingu subdomen, którą możesz stworzyć taką trasę. Działa obecnie dla .NET Core 1.1 i .NET Framework 4.6.1, ale zostanie zaktualizowany w najbliższej przyszłości. Jak to działa:
1) Mapuj trasę do subdomeny w Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    var hostnames = new[] { "localhost:54575" };

    app.UseMvc(routes =>
    {
        routes.MapSubdomainRoute(
            hostnames,
            "SubdomainRoute",
            "{username}",
            "{controller}/{action}",
            new { controller = "Home", action = "Index" });
    )};

2) Kontrolery / HomeController.cs

public IActionResult Index(string username)
{
    //code
}

3) Ta biblioteka umożliwi także generowanie adresów URL i formularzy. Kod:

@Html.ActionLink("User home", "Index", "Home" new { username = "user1" }, null)

Wygeneruje <a href="http://user1.localhost:54575/Home/Index">User home</a> Wygenerowano URL będzie również zależeć od aktualnej pozycji przyjmującego i schematu.
Możesz także użyć pomocników HTML dla BeginFormi UrlHelper. Jeśli chcesz, możesz także skorzystać z nowej funkcji o nazwie tag helpers ( FormTagHelper, AnchorTagHelper)
Ta biblioteka nie ma jeszcze żadnej dokumentacji, ale istnieją pewne testy i przykładowy projekt, więc możesz ją odkryć.

Mariusz
źródło
2

W ASP.NET Core host jest dostępny za pośrednictwem Request.Host.Host. Jeśli chcesz zezwolić na zastąpienie hosta za pomocą parametru zapytania, najpierw sprawdź Request.Query.

Aby spowodować propagację parametru zapytania hosta do nowych adresów URL opartych na trasach, dodaj ten kod do app.UseMvckonfiguracji trasy:

routes.Routes.Add(new HostPropagationRouter(routes.DefaultHandler));

I zdefiniuj w HostPropagationRouterten sposób:

/// <summary>
/// A router that propagates the request's "host" query parameter to the response.
/// </summary>
class HostPropagationRouter : IRouter
{
    readonly IRouter router;

    public HostPropagationRouter(IRouter router)
    {
        this.router = router;
    }

    public VirtualPathData GetVirtualPath(VirtualPathContext context)
    {
        if (context.HttpContext.Request.Query.TryGetValue("host", out var host))
            context.Values["host"] = host;
        return router.GetVirtualPath(context);
    }

    public Task RouteAsync(RouteContext context) => router.RouteAsync(context);
}
Edward Brey
źródło
1

Po zdefiniowaniu nowego modułu obsługi trasy, który będzie patrzył na host przekazany w adresie URL , możesz przejść do pomysłu podstawowego kontrolera, który jest świadomy witryny, do której jest uzyskiwany dostęp. To wygląda tak:

public abstract class SiteController : Controller {
    ISiteProvider _siteProvider;

    public SiteController() {
        _siteProvider = new SiteProvider();
    }

    public SiteController(ISiteProvider siteProvider) {
        _siteProvider = siteProvider;
    }

    protected override void Initialize(RequestContext requestContext) {
        string[] host = requestContext.HttpContext.Request.Headers["Host"].Split(':');

        _siteProvider.Initialise(host[0]);

        base.Initialize(requestContext);
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext) {
        ViewData["Site"] = Site;

        base.OnActionExecuting(filterContext);
    }

    public Site Site {
        get {
            return _siteProvider.GetCurrentSite();
        }
    }

}

ISiteProvider to prosty interfejs:

public interface ISiteProvider {
    void Initialise(string host);
    Site GetCurrentSite();
}

Polecam ci przejście do bloga Luke'a Sampsona

Amirhossein Mehrvarzi
źródło
1

Jeśli chcesz nadać projektowi możliwości MultiTenancy z różnymi domenami / poddomenami dla każdego najemcy, zapoznaj się z SaasKit:

https://github.com/saaskit/saaskit

Przykłady kodu można zobaczyć tutaj: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy

Niektóre przykłady wykorzystujące rdzeń ASP.NET: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/

EDYCJA: Jeśli nie chcesz używać SaasKit w głównym projekcie ASP.NET, możesz zapoznać się z implementacją routingu domen Maarten dla MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain -routing-and-resolving-current-tenant-with-aspnet-mvc-6-aspnet-5.html

Jednak te Gists nie są utrzymywane i należy je dostosować, aby działały z najnowszą wersją rdzenia ASP.NET.

Bezpośredni link do kodu: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs

Darxtar
źródło
Nie szukam multitenancy - ale dziękuję za podpowiedź!
Dan Esparza
0

Kilka miesięcy temu opracowałem atrybut, który ogranicza metody lub kontrolery do określonych domen.

Jest dość łatwy w użyciu:

[IsDomain("localhost","example.com","www.example.com","*.t1.example.com")]
[HttpGet("RestrictedByHost")]
public IActionResult Test(){}

Możesz także zastosować go bezpośrednio na kontrolerze.

public class IsDomainAttribute : Attribute, Microsoft.AspNetCore.Mvc.Filters.IAuthorizationFilter
{

    public IsDomainAttribute(params string[]  domains)
    {
        Domains = domains;
    }

    public string[] Domains { get; }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var host = context.HttpContext.Request.Host.Host;
        if (Domains.Contains(host))
            return;
        if (Domains.Any(d => d.EndsWith("*"))
                && Domains.Any(d => host.StartsWith(d.Substring(0, d.Length - 1))))
            return;
        if (Domains.Any(d => d.StartsWith("*"))
                && Domains.Any(d => host.EndsWith(d.Substring(1))))
            return;

        context.Result = new Microsoft.AspNetCore.Mvc.NotFoundResult();//.ChallengeResult
    }
}

Ograniczenie: możesz nie być w stanie mieć dwóch takich samych tras dla różnych metod z różnymi filtrami Mam na myśli, że poniższe mogą zgłaszać wyjątek dla zduplikowanej trasy:

[IsDomain("test1.example.com")]
[HttpGet("/Test")]
public IActionResult Test1(){}

[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}
Drelich
źródło