Zdaję sobie sprawę, że sesja i REST nie idą w parze, ale czy nie można uzyskać dostępu do stanu sesji przy użyciu nowego interfejsu API sieci Web? HttpContext.Current.Session
jest zawsze zerowy.
Zdaję sobie sprawę, że sesja i REST nie idą w parze, ale czy nie można uzyskać dostępu do stanu sesji przy użyciu nowego interfejsu API sieci Web? HttpContext.Current.Session
jest zawsze zerowy.
Odpowiedzi:
MVC
W przypadku projektu MVC wprowadź następujące zmiany (poniżej znajdują się odpowiedzi na formularze WebForms i Dot Net Core):
public static class WebApiConfig
{
public static string UrlPrefix { get { return "api"; } }
public static string UrlPrefixRelative { get { return "~/api"; } }
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: WebApiConfig.UrlPrefix + "/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
public class MvcApplication : System.Web.HttpApplication
{
...
protected void Application_PostAuthorizeRequest()
{
if (IsWebApiRequest())
{
HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required);
}
}
private bool IsWebApiRequest()
{
return HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath.StartsWith(WebApiConfig.UrlPrefixRelative);
}
}
To rozwiązanie ma dodatkową zaletę, że możemy pobrać podstawowy adres URL w javascript do wykonywania wywołań AJAX:
<body>
@RenderBody()
<script type="text/javascript">
var apiBaseUrl = '@Url.Content(ProjectNameSpace.WebApiConfig.UrlPrefixRelative)';
</script>
@RenderSection("scripts", required: false)
a następnie w plikach / kodzie JavaScript możemy wykonywać nasze wywołania webapi, które mogą uzyskać dostęp do sesji:
$.getJSON(apiBaseUrl + '/MyApi')
.done(function (data) {
alert('session data received: ' + data.whatever);
})
);
Formularze internetowe
Wykonaj powyższe czynności, ale zmień funkcję WebApiConfig.Register, aby zamiast tego pobrać RouteCollection:
public static void Register(RouteCollection routes)
{
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: WebApiConfig.UrlPrefix + "/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
A następnie wywołaj następujące w Application_Start:
WebApiConfig.Register(RouteTable.Routes);
Dot Net Core
Dodaj pakiet NuGet Microsoft.AspNetCore.Session , a następnie wprowadź następujące zmiany kodu:
Wywołaj metody AddDistributMemoryCache i AddSession w obiekcie usług w ramach funkcji ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
...
services.AddDistributedMemoryCache();
services.AddSession();
i w funkcji Konfiguruj dodaj wywołanie do UseSession :
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
app.UseSession();
app.UseMvc();
W swoim kontrolerze dodaj instrukcję using u góry:
using Microsoft.AspNetCore.Http;
a następnie użyj obiektu HttpContext.Session w kodzie w następujący sposób:
[HttpGet("set/{data}")]
public IActionResult setsession(string data)
{
HttpContext.Session.SetString("keyname", data);
return Ok("session data set");
}
[HttpGet("get")]
public IActionResult getsessiondata()
{
var sessionData = HttpContext.Session.GetString("keyname");
return Ok(sessionData);
}
powinieneś teraz być w stanie trafić:
http://localhost:1234/api/session/set/thisissomedata
a następnie przejście do tego adresu URL spowoduje jego wyciągnięcie:
http://localhost:1234/api/session/get
Więcej informacji na temat uzyskiwania dostępu do danych sesji w ramach dot net core tutaj: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state
Obawy dotyczące wydajności
Przeczytaj poniżej odpowiedź Simona Weavera dotyczącą wydajności. Jeśli uzyskujesz dostęp do danych sesji w projekcie WebApi, może to mieć bardzo poważny wpływ na wydajność - widziałem, jak ASP.NET wymusza opóźnienie 200 ms dla równoczesnych żądań. Może się to sumować i stać się katastrofalne, jeśli masz wiele równoczesnych żądań.
Obawy dotyczące bezpieczeństwa
Upewnij się, że blokujesz zasoby na użytkownika - uwierzytelniony użytkownik nie powinien mieć możliwości pobierania danych z WebApi, do których nie ma dostępu.
Przeczytaj artykuł Microsoftu na temat uwierzytelniania i autoryzacji w ASP.NET Web API - https://www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api
Przeczytaj artykuł Microsoftu na temat unikania ataków hakerskich na żądanie krzyżowej witryny. (W skrócie, sprawdź metodę AntiForgery.Validate) - https://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-csrf-attacks
Dostęp do stanu sesji można uzyskać za pomocą niestandardowego narzędzia RouteHandler.
// In global.asax
public class MvcApp : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
var route = routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
route.RouteHandler = new MyHttpControllerRouteHandler();
}
}
// Create two new classes
public class MyHttpControllerHandler
: HttpControllerHandler, IRequiresSessionState
{
public MyHttpControllerHandler(RouteData routeData) : base(routeData)
{ }
}
public class MyHttpControllerRouteHandler : HttpControllerRouteHandler
{
protected override IHttpHandler GetHttpHandler(
RequestContext requestContext)
{
return new MyHttpControllerHandler(requestContext.RouteData);
}
}
// Now Session is visible in your Web API
public class ValuesController : ApiController
{
public string Get(string input)
{
var session = HttpContext.Current.Session;
if (session != null)
{
if (session["Time"] == null)
session["Time"] = DateTime.Now;
return "Session Time: " + session["Time"] + input;
}
return "Session is not availabe" + input;
}
}
Znalezione tutaj: http://techhasnoboundary.blogspot.com/2012/03/mvc-4-web-api-access-session.html
Wydajność, wydajność, wydajność!
Jest bardzo dobry i często pomijany powód, dla którego nie powinieneś w ogóle używać Sesji w WebAPI.
Sposób działania ASP.NET podczas korzystania z sesji polega na serializacji wszystkich żądań otrzymanych od jednego klienta . Teraz nie mówię o serializacji obiektów, ale uruchamiam je w otrzymanej kolejności i czekam na zakończenie każdego z nich przed uruchomieniem następnego. Ma to na celu uniknięcie nieprzyjemnych warunków dotyczących wątku / wyścigu, jeśli dwa żądania próbują uzyskać dostęp do sesji jednocześnie.
Współbieżne żądania i stan sesji
Dostęp do stanu sesji ASP.NET jest wyłączny dla każdej sesji, co oznacza, że jeśli dwóch różnych użytkowników zgłasza współbieżne żądania, dostęp do każdej oddzielnej sesji jest przyznawany jednocześnie. Jeśli jednak dla tej samej sesji zostaną wysłane dwa jednoczesne żądania (przy użyciu tej samej wartości SessionID), pierwsze żądanie uzyska wyłączny dostęp do informacji o sesji. Drugie żądanie zostanie wykonane dopiero po zakończeniu pierwszego żądania.(Druga sesja może również uzyskać dostęp, jeśli wyłączna blokada informacji zostanie zwolniona, ponieważ pierwsze żądanie przekroczy limit czasu blokady.) Jeśli wartość EnableSessionState w dyrektywie @ Page jest ustawiona na Tylko do odczytu, żądanie tylko do odczytu informacje o sesji nie powodują wyłącznej blokady danych sesji. Jednak żądania tylko do odczytu danych sesji mogą nadal wymagać oczekiwania na ustawienie blokady przez żądanie odczytu i zapisu dla danych sesji.
Co to oznacza dla Web API? Jeśli masz aplikację obsługującą wiele żądań AJAX, tylko JEDNA będzie mogła być uruchomiona jednocześnie. Jeśli masz wolniejsze żądanie, spowoduje to zablokowanie wszystkich pozostałych klientów tego klienta, dopóki nie zostanie zrealizowane. W niektórych aplikacjach może to prowadzić do bardzo zauważalnie niskiej wydajności.
Dlatego prawdopodobnie powinieneś użyć kontrolera MVC, jeśli absolutnie potrzebujesz czegoś z sesji użytkownika i uniknąć niepotrzebnej utraty wydajności w przypadku włączenia go dla WebApi.
Możesz to łatwo sprawdzić sam, wprowadzając Thread.Sleep(5000)
metodę WebAPI i włączając sesję. Wykonaj do niego 5 próśb, a ich wypełnienie zajmie 25 sekund. Bez sesji zajmie to nieco ponad 5 sekund.
(To samo rozumowanie dotyczy SignalR).
Masz rację, REST jest bezpaństwowcem. Jeśli użyjesz sesji, przetwarzanie stanie się stanowe, kolejne żądania będą mogły użyć stanu (z sesji).
Aby sesja została uwodniona, musisz podać klucz do powiązania stanu. W normalnej aplikacji asp.net klucz ten jest dostarczany za pomocą pliku cookie (sesje cookie) lub parametru adresu URL (sesje bez plików cookie).
Jeśli potrzebujesz sesji zapomnij o odpoczynku, sesje nie mają znaczenia w projektach opartych na REST. Jeśli potrzebujesz sesji do weryfikacji, użyj tokena lub autoryzuj według adresów IP.
Zaznacz, jeśli sprawdzisz przykład MVC nerddinner, logika jest prawie taka sama.
Wystarczy pobrać plik cookie i ustawić go w bieżącej sesji.
Global.asax.cs
public override void Init()
{
this.AuthenticateRequest += new EventHandler(WebApiApplication_AuthenticateRequest);
base.Init();
}
void WebApiApplication_AuthenticateRequest(object sender, EventArgs e)
{
HttpCookie cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
SampleIdentity id = new SampleIdentity(ticket);
GenericPrincipal prin = new GenericPrincipal(id, null);
HttpContext.Current.User = prin;
}
enter code here
Musisz zdefiniować klasę „SampleIdentity”, którą możesz pożyczyć z projektu nerddinner .
Aby rozwiązać problem:
protected void Application_PostAuthorizeRequest()
{
System.Web.HttpContext.Current.SetSessionStateBehavior(System.Web.SessionState.SessionStateBehavior.Required);
}
w Global.asax.cs
Ostatni nie działa teraz, weź ten, działał dla mnie.
w WebApiConfig.cs w App_Start
public static string _WebApiExecutionPath = "api";
public static void Register(HttpConfiguration config)
{
var basicRouteTemplate = string.Format("{0}/{1}", _WebApiExecutionPath, "{controller}");
// Controller Only
// To handle routes like `/api/VTRouting`
config.Routes.MapHttpRoute(
name: "ControllerOnly",
routeTemplate: basicRouteTemplate//"{0}/{controller}"
);
// Controller with ID
// To handle routes like `/api/VTRouting/1`
config.Routes.MapHttpRoute(
name: "ControllerAndId",
routeTemplate: string.Format ("{0}/{1}", basicRouteTemplate, "{id}"),
defaults: null,
constraints: new { id = @"^\d+$" } // Only integers
);
Global.asax
protected void Application_PostAuthorizeRequest()
{
if (IsWebApiRequest())
{
HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required);
}
}
private static bool IsWebApiRequest()
{
return HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath.StartsWith(_WebApiExecutionPath);
}
po czwarte tutaj: http://forums.asp.net/t/1773026.aspx/1
Zgodnie z odpowiedzią LachlanB, jeśli Twój ApiController nie znajduje się w określonym katalogu (jak / api), możesz zamiast tego przetestować żądanie za pomocą RouteTable.Routes.GetRouteData, na przykład:
protected void Application_PostAuthorizeRequest()
{
// WebApi SessionState
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(HttpContext.Current));
if (routeData != null && routeData.RouteHandler is HttpControllerRouteHandler)
HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required);
}
Miałem ten sam problem w asp.net mvc, naprawiłem go, umieszczając tę metodę w moim podstawowym kontrolerze interfejsu API, który dziedziczą wszystkie moje kontrolery interfejsu API:
/// <summary>
/// Get the session from HttpContext.Current, if that is null try to get it from the Request properties.
/// </summary>
/// <returns></returns>
protected HttpContextWrapper GetHttpContextWrapper()
{
HttpContextWrapper httpContextWrapper = null;
if (HttpContext.Current != null)
{
httpContextWrapper = new HttpContextWrapper(HttpContext.Current);
}
else if (Request.Properties.ContainsKey("MS_HttpContext"))
{
httpContextWrapper = (HttpContextWrapper)Request.Properties["MS_HttpContext"];
}
return httpContextWrapper;
}
Następnie w interfejsie API, który chcesz uzyskać dostęp do sesji, po prostu wykonaj:
HttpContextWrapper httpContextWrapper = GetHttpContextWrapper();
var someVariableFromSession = httpContextWrapper.Session["SomeSessionValue"];
Mam to również w moim pliku Global.asax.cs, tak jak inne osoby, nie jestem pewien, czy nadal potrzebujesz go za pomocą powyższej metody, ale tutaj jest tak na wszelki wypadek:
/// <summary>
/// The following method makes Session available.
/// </summary>
protected void Application_PostAuthorizeRequest()
{
if (HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath.StartsWith("~/api"))
{
HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required);
}
}
Możesz także utworzyć niestandardowy atrybut filtru, który możesz trzymać na wywołaniach interfejsu API potrzebnych do sesji, a następnie możesz użyć sesji w wywołaniu interfejsu API, tak jak zwykle za pośrednictwem HttpContext.Current.Session [„SomeValue”]:
/// <summary>
/// Filter that gets session context from request if HttpContext.Current is null.
/// </summary>
public class RequireSessionAttribute : ActionFilterAttribute
{
/// <summary>
/// Runs before action
/// </summary>
/// <param name="actionContext"></param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (HttpContext.Current == null)
{
if (actionContext.Request.Properties.ContainsKey("MS_HttpContext"))
{
HttpContext.Current = ((HttpContextWrapper)actionContext.Request.Properties["MS_HttpContext"]).ApplicationInstance.Context;
}
}
}
}
Mam nadzieję że to pomoże.
Postępowałem zgodnie z podejściem @LachlanB i rzeczywiście sesja była dostępna, gdy plik cookie sesji był obecny na żądanie. Brakuje tego, w jaki sposób plik cookie sesji jest wysyłany do klienta po raz pierwszy?
Utworzyłem HttpModule, który nie tylko włącza dostępność HttpSessionState, ale także wysyła ciasteczko do klienta, gdy tworzona jest nowa sesja.
public class WebApiSessionModule : IHttpModule
{
private static readonly string SessionStateCookieName = "ASP.NET_SessionId";
public void Init(HttpApplication context)
{
context.PostAuthorizeRequest += this.OnPostAuthorizeRequest;
context.PostRequestHandlerExecute += this.PostRequestHandlerExecute;
}
public void Dispose()
{
}
protected virtual void OnPostAuthorizeRequest(object sender, EventArgs e)
{
HttpContext context = HttpContext.Current;
if (this.IsWebApiRequest(context))
{
context.SetSessionStateBehavior(SessionStateBehavior.Required);
}
}
protected virtual void PostRequestHandlerExecute(object sender, EventArgs e)
{
HttpContext context = HttpContext.Current;
if (this.IsWebApiRequest(context))
{
this.AddSessionCookieToResponseIfNeeded(context);
}
}
protected virtual void AddSessionCookieToResponseIfNeeded(HttpContext context)
{
HttpSessionState session = context.Session;
if (session == null)
{
// session not available
return;
}
if (!session.IsNewSession)
{
// it's safe to assume that the cookie was
// received as part of the request so there is
// no need to set it
return;
}
string cookieName = GetSessionCookieName();
HttpCookie cookie = context.Response.Cookies[cookieName];
if (cookie == null || cookie.Value != session.SessionID)
{
context.Response.Cookies.Remove(cookieName);
context.Response.Cookies.Add(new HttpCookie(cookieName, session.SessionID));
}
}
protected virtual string GetSessionCookieName()
{
var sessionStateSection = (SessionStateSection)ConfigurationManager.GetSection("system.web/sessionState");
return sessionStateSection != null && !string.IsNullOrWhiteSpace(sessionStateSection.CookieName) ? sessionStateSection.CookieName : SessionStateCookieName;
}
protected virtual bool IsWebApiRequest(HttpContext context)
{
string requestPath = context.Request.AppRelativeCurrentExecutionFilePath;
if (requestPath == null)
{
return false;
}
return requestPath.StartsWith(WebApiConfig.UrlPrefixRelative, StringComparison.InvariantCultureIgnoreCase);
}
}
jedną rzecz należy wspomnieć w odpowiedzi @LachlanB.
protected void Application_PostAuthorizeRequest()
{
if (IsWebApiRequest())
{
HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required);
}
}
Jeśli pominiesz linię if (IsWebApiRequest())
W całej witrynie występuje problem spowolnienia ładowania strony, jeśli jest ona pomieszana ze stronami formularzy internetowych.
Tak, sesja nie idzie w parze z Rest API, a także powinniśmy unikać tych praktyk. Ale zgodnie z wymaganiami musimy w jakiś sposób utrzymywać sesję, aby na każde żądanie serwer klienta mógł wymieniać lub utrzymywać stan lub dane. Tak więc najlepszym sposobem na osiągnięcie tego bez złamania protokołów REST jest komunikacja za pomocą tokena, takiego jak JWT.
Wracając do podstaw, dlaczego nie uprościć tego i zapisać wartość sesji w ukrytej wartości HTML, aby przekazać ją do interfejsu API?
Kontroler
public ActionResult Index()
{
Session["Blah"] = 609;
YourObject yourObject = new YourObject();
yourObject.SessionValue = int.Parse(Session["Blah"].ToString());
return View(yourObject);
}
cshtml
@model YourObject
@{
var sessionValue = Model.SessionValue;
}
<input type="hidden" value="@sessionValue" id="hBlah" />
JavaScript
$ (dokument) .ready (function () {
var sessionValue = $('#hBlah').val();
alert(sessionValue);
/* Now call your API with the session variable */}
}
[SessionState(SessionStateBehavior.Required)]
naApiController
lewę (lub w.ReadOnly
razie potrzeby).