Я понимаю, что сессия и REST точно не идут рука об руку, но нельзя ли получить доступ к состоянию сеанса с помощью нового Web API? HttpContext.Current.Session
всегда ноль.
Я понимаю, что сессия и REST точно не идут рука об руку, но нельзя ли получить доступ к состоянию сеанса с помощью нового Web API? HttpContext.Current.Session
всегда ноль.
Ответы:
MVC
Для проекта MVC внесите следующие изменения (ответ WebForms и 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);
}
}
Это решение имеет дополнительный бонус, который мы можем получить базовый URL в JavaScript для выполнения вызовов AJAX:
<body>
@RenderBody()
<script type="text/javascript">
var apiBaseUrl = '@Url.Content(ProjectNameSpace.WebApiConfig.UrlPrefixRelative)';
</script>
@RenderSection("scripts", required: false)
и затем в наших файлах / коде Javascript мы можем сделать наши вызовы webapi, которые могут получить доступ к сеансу:
$.getJSON(apiBaseUrl + '/MyApi')
.done(function (data) {
alert('session data received: ' + data.whatever);
})
);
WebForms
Выполните вышеописанное, но измените функцию WebApiConfig.Register, чтобы вместо нее использовать RouteCollection:
public static void Register(RouteCollection routes)
{
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: WebApiConfig.UrlPrefix + "/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
А затем вызовите следующее в Application_Start:
WebApiConfig.Register(RouteTable.Routes);
Dot Net Core
Добавьте пакет Microsoft.AspNetCore.Session NuGet и внесите следующие изменения в код:
Вызовите методы AddDistributedMemoryCache и AddSession для объекта служб в функции ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
...
services.AddDistributedMemoryCache();
services.AddSession();
и в функции Configure добавьте вызов UseSession :
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
app.UseSession();
app.UseMvc();
В вашем контроллере добавьте оператор использования вверху:
using Microsoft.AspNetCore.Http;
и затем используйте объект HttpContext.Session в вашем коде следующим образом:
[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);
}
теперь вы должны быть в состоянии ударить:
http://localhost:1234/api/session/set/thisissomedata
и затем, перейдя по этому URL, вытащите его:
http://localhost:1234/api/session/get
Здесь можно найти больше информации о доступе к данным сеанса в ядре dot net: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state.
Проблемы производительности
Прочитайте ответ Саймона Уивера ниже относительно производительности. Если вы обращаетесь к данным сеанса внутри проекта WebApi, это может иметь очень серьезные последствия для производительности - я видел, как ASP.NET применяет задержку 200 мс для одновременных запросов. Это может привести к катастрофическим последствиям, если у вас много одновременных запросов.
Проблемы безопасности
Убедитесь, что вы блокируете ресурсы для каждого пользователя - аутентифицированный пользователь не должен иметь возможность получать данные из вашего WebApi, к которым у них нет доступа.
Прочитайте статью Microsoft по аутентификации и авторизации в ASP.NET Web API - https://www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api.
Прочитайте статью Microsoft о том, как избежать хакерских атак на межсайтовые запросы. (Короче, проверьте метод AntiForgery.Validate) - https://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-csrf-attacks
Вы можете получить доступ к состоянию сеанса с помощью пользовательского 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;
}
}
Найдено здесь: http://techhasnoboundary.blogspot.com/2012/03/mvc-4-web-api-access-session.html
Производительность, производительность, производительность!
Есть очень хорошая и часто упускаемая из виду причина, по которой вам вообще не следует использовать Session в WebAPI.
ASP.NET работает, когда Session используется, чтобы сериализовать все запросы, полученные от одного клиента . Сейчас я не говорю о сериализации объектов - но запускаю их в порядке поступления и жду завершения каждого из них, прежде чем запускать следующий. Это делается для того, чтобы избежать неприятных условий потока / гонки, если два запроса каждый пытаются получить доступ к Сессии одновременно.
Параллельные запросы и состояние сеанса
Доступ к состоянию сеанса ASP.NET является исключительным для каждого сеанса, что означает, что если два разных пользователя делают параллельные запросы, доступ к каждому отдельному сеансу предоставляется одновременно. Однако, если два одновременных запроса сделаны для одного и того же сеанса (с использованием одного и того же значения SessionID), первый запрос получает эксклюзивный доступ к информации сеанса. Второй запрос выполняется только после того, как первый запрос завершен.(Второй сеанс также может получить доступ, если исключительная блокировка информации освобождается, поскольку первый запрос превышает время ожидания блокировки.) Если значение EnableSessionState в директиве @ Page установлено в ReadOnly, запрос только для чтения. Информация о сеансе не приводит к исключительной блокировке данных сеанса. Однако запросы только для чтения для данных сеанса, возможно, все еще должны ждать блокировки, установленной запросом чтения-записи, для очистки данных сеанса.
Так что же это значит для веб-API? Если у вас есть приложение, выполняющее много запросов AJAX, тогда только ОДИН сможет работать одновременно. Если у вас более медленный запрос, он будет блокировать всех остальных от этого клиента, пока он не будет завершен. В некоторых приложениях это может привести к очень заметному снижению производительности.
Поэтому вам, вероятно, следует использовать контроллер MVC, если вам абсолютно необходимо что-то из пользовательского сеанса и избежать ненужного снижения производительности при включении его для WebApi.
Вы можете легко проверить это сами, просто Thread.Sleep(5000)
вставив метод WebAPI и включив Session. Выполните 5 запросов к нему, и на их выполнение уйдет всего 25 секунд. Без сеанса они займут чуть более 5 секунд.
(То же самое относится и к SignalR).
Ну, вы правы, REST без гражданства. Если вы используете сеанс, обработка станет с состоянием, последующие запросы смогут использовать состояние (из сеанса).
Для того, чтобы сеанс был перегидратирован, вам нужно будет предоставить ключ, чтобы связать состояние. В обычном приложении asp.net этот ключ предоставляется с помощью cookie (cookie-сессий) или параметра url (сессий без cookie).
Если вам нужен сеанс, забудьте про отдых, сеансы не имеют отношения к проектам на основе REST. Если вам нужен сеанс для проверки, используйте токен или авторизуйтесь по IP-адресам.
Марк, если вы посмотрите на пример MVC nerddinner, логика будет почти такой же.
Вам нужно только извлечь cookie и установить его в текущем сеансе.
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
Вы должны будете определить свой класс "SampleIdentity", который вы можете позаимствовать из проекта nerddinner .
Чтобы исправить проблему:
protected void Application_PostAuthorizeRequest()
{
System.Web.HttpContext.Current.SetSessionStateBehavior(System.Web.SessionState.SessionStateBehavior.Required);
}
в Global.asax.cs
Последний сейчас не работает, возьми этот, он работал для меня.
в WebApiConfig.cs на 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);
}
Четвертый здесь: http://forums.asp.net/t/1773026.aspx/1
Исходя из ответа LachlanB, если ваш ApiController не находится в определенном каталоге (например, / api), вы можете вместо этого протестировать запрос, используя RouteTable.Routes.GetRouteData, например:
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);
}
У меня была такая же проблема в asp.net mvc, я исправил ее, поместив этот метод в свой базовый контроллер API, от которого наследуются все мои контроллеры 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;
}
Затем в вызове API вы хотите получить доступ к сеансу, который вы просто делаете:
HttpContextWrapper httpContextWrapper = GetHttpContextWrapper();
var someVariableFromSession = httpContextWrapper.Session["SomeSessionValue"];
У меня также есть это в моем файле Global.asax.cs, как и другие люди, не уверен, если вам все еще нужно, используя метод, описанный выше, но здесь это на всякий случай:
/// <summary>
/// The following method makes Session available.
/// </summary>
protected void Application_PostAuthorizeRequest()
{
if (HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath.StartsWith("~/api"))
{
HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required);
}
}
Вы также можете просто создать собственный атрибут фильтра, который вы можете прикрепить к вызовам API, для которого вам нужен сеанс, затем вы можете использовать сеанс в вызове API, как обычно, через 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;
}
}
}
}
Надеюсь это поможет.
Я следовал подходу @LachlanB, и действительно, сессия была доступна, когда в запросе присутствовал сессионный cookie. Недостающая часть - как файл cookie сеанса отправляется клиенту в первый раз?
Я создал HttpModule, который не только включает доступность HttpSessionState, но и отправляет cookie клиенту при создании нового сеанса.
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);
}
}
В ответе @LachlanB нужно упомянуть одну вещь.
protected void Application_PostAuthorizeRequest()
{
if (IsWebApiRequest())
{
HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required);
}
}
Если вы пропустите строку if (IsWebApiRequest())
Весь сайт будет иметь проблемы с медлительностью загрузки страницы, если ваш сайт смешан со страницами веб-формы.
Да, сессия не идет рука об руку с Rest API, и мы также должны избегать этой практики. Но в соответствии с требованиями нам нужно как-то поддерживать сеанс так, чтобы в каждом запросе клиентский сервер мог обмениваться или поддерживать состояние или данные. Таким образом, лучший способ достичь этого без нарушения протоколов REST - это общение через токен, такой как JWT.
Возвращаясь к основам, почему бы не сделать это простым и сохранить значение Session в скрытом значении html для передачи вашему API?
контроллер
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
$ (документ) .ready (function () {
var sessionValue = $('#hBlah').val();
alert(sessionValue);
/* Now call your API with the session variable */}
}
[SessionState(SessionStateBehavior.Required)]
наApiController
делает трюк (или,.ReadOnly
где это уместно).