Доступ к сеансу с помощью ASP.NET Web API

208

Я понимаю, что сеанс и REST не идут рука об руку, но разве невозможно получить доступ к состоянию сеанса с помощью нового веб-API? HttpContext.Current.Session всегда имеет значение null.

  • 3
    [SessionState(SessionStateBehavior.Required)] на ApiController делает ApiController дело (или .ReadOnly где это уместно).
  • 0
    @RomanStarkov Не удалось заставить это работать. Какую среду вы использовали? .NET Core?
Показать ещё 3 комментария
Теги:
asp.net-web-api

12 ответов

231
Лучший ответ

MVC

Для проекта MVC сделайте следующие изменения (ниже приведены WebForms и Dot Net Core):

WebApiConfig.cs

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 }
        );
    }
}

Global.asax.cs

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:

_Layout.cshtml

<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);

Точечное сетевое ядро ​​

Добавьте пакет Microsoft.AspNetCore.Session NuGet, а затем выполните следующие изменения кода:

Startup.cs

Вызовите методы AddDistributedMemoryCache и AddSession для объекта services в функции 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();

SessionController.cs

Внутри вашего контроллера добавьте оператор using вверху:

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 - 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

  • 0
    Я получил ошибку в шаблоне RIS не существует. Я экстремальный n00b и не понимал, что вы должны заменить это на имя вашего проекта, если кто-то столкнется с этим
  • 0
    Извините за то, что Jameo, я обновил его, чтобы сказать ProjectNameSpace, так что это должно быть немного более очевидным.
Показать ещё 16 комментариев
58

Вы можете получить доступ к состоянию сеанса с помощью настраиваемого 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

  • 12
    Обновление: если ваши функции API читают из сеанса и не изменяют сеанс, было бы неплохо использовать IReadOnlySessionState вместо IRequiresSessionState. Это гарантирует, что сеанс не заблокирован во время обработки функции API.
  • 6
    не работает для меня в MVC 4 - route.RouteHandler даже не собственность для меня. @LachlanB, похоже, работает на меня.
Показать ещё 4 комментария
28

Почему бы избежать использования сеанса в WebAPI?

Производительность, производительность, производительность!

Там очень хорошая и часто забываемая причина, почему вы вообще не должны использовать Session в WebAPI.

Способ ASP.NET, когда сеанс используется, - это сериализовать все запросы, полученные от одного клиента. Теперь я не говорю о сериализации объектов, но выполняю их в полученном порядке и ожидаю завершения каждого из них до запуска следующего. Это делается для того, чтобы избежать неприятных условий нити/гонки, если два запроса каждый раз пытаются получить доступ к сеансу одновременно.

Параллельные запросы и состояние сеанса

Доступ к состоянию сеанса ASP.NET является исключительным за сеанс, что означает, что если два разных пользователя одновременные запросы, предоставляется доступ к каждому отдельному сеансу одновременно. Однако , если два параллельных запроса сделаны для тот же сеанс (с использованием того же значения SessionID), первый запрос получает эксклюзивный доступ к информации о сеансе. Второй запрос выполняется только после завершения первого запроса. (второй сеанс также может получить доступ, если исключить исключительную блокировку информации потому что первый запрос превышает тайм-аут блокировки.) Если Значение EnableSessionState в директиве @Страница установлено в ReadOnly, a запрос для информации сеанса только для чтения не приводит к исключительная блокировка данных сеанса. Однако запросы только для чтения для сессионным данным все равно придется ждать блокировки, установленной чтением-записью запрос на очистку данных сеанса.

Итак, что это значит для Web API? Если у вас есть приложение, на котором запущено множество запросов AJAX, тогда только одно может работать одновременно. Если у вас более медленный запрос, он блокирует все остальные, пока он не будет завершен. В некоторых приложениях это может привести к очень заметной вялости производительности.

Таким образом, вы должны, вероятно, использовать контроллер MVC, если вам абсолютно нужно что-то из сеанса пользователей и избежать лишнего снижения производительности, чтобы включить его для WebApi.

Вы можете легко проверить это самостоятельно, просто поместив Thread.Sleep(5000) в метод WebAPI и включите сеанс. Запустите 5 запросов, и они займут в общей сложности 25 секунд. Без сеанса они занимают в общей сложности чуть более 5 секунд.

(Это же рассуждение относится к SignalR).

  • 15
    Вы можете обойти это, используя [SessionState (SessionStateBehavior.ReadOnly)], если ваш метод читает только из сеанса.
21

Отметьте, если вы проверите пример nerddinner MVC, логика почти такая же.

Вам нужно всего лишь восстановить файл 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.

  • 2
    -> stackoverflow.com/a/9594406/324238 <-
  • 0
    Класс идентификации находится в NerdDinner_2.0 \ NerdDinner \ Models \ NerdIdentity.cs.
Показать ещё 3 комментария
21

Хорошо, что вы правы, REST не имеет гражданства. Если вы используете сеанс, обработка будет выполнена с сохранением состояния, последующие запросы смогут использовать состояние (из сеанса).

Для того, чтобы сеанс был регидратирован, вам нужно предоставить ключ для связывания состояния. В обычном приложении asp.net этот ключ предоставляется с использованием файлов cookie (cookie-сессий) или параметров url (сеансы cookieless).

Если вам нужна сессия, забудьте отдохнуть, сеансы не имеют отношения к проектам на основе REST. Если вам нужна сессия для проверки, используйте маркер или авторизацию по IP-адресам.

  • 10
    Я не уверен в этом. В примерах Microsoft они показывают использование атрибута Authorize. Я пробовал это, и это работает с проверкой подлинности на основе форм. Веб-API знает о состоянии аутентификации, которое передается в куки-файле аутентификации по умолчанию.
  • 4
    Вот пример, на который я ссылаюсь: code.msdn.microsoft.com/ASPNET-Web-API-JavaScript-d0d64dd7 . Он использует новый веб-API на основе REST, реализующий проверку подлинности с помощью форм.
Показать ещё 9 комментариев
9

Чтобы устранить проблему:

protected void Application_PostAuthorizeRequest()
{
    System.Web.HttpContext.Current.SetSessionStateBehavior(System.Web.SessionState.SessionStateBehavior.Required);
}

в Global.asax.cs

  • 2
    Предупреждение! Это включит сеанс для ВСЕХ запросов. Это действительно может снизить производительность, если ваше приложение использует встроенные ресурсы.
  • 0
    @cgatian какое-нибудь альтернативное решение исправлено ?
Показать ещё 1 комментарий
9

Последний не работает сейчас, возьмите это, он сработал у меня.

в 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

  • 0
    Это простейшее решение, но в коде есть несколько ошибок, так что оно на самом деле не работает. Я опубликовал другое решение, основанное на этом, не стесняйтесь редактировать свое, чтобы оно соответствовало моему.
  • 0
    Небольшое исправление в строке _WebApiExecutionPath должно считывать открытую статическую строку _WebApiExecutionPath = "~ / api";
8

Следуя ответу 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);
    }
6

У меня была такая же проблема в 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;
        }
      }
    }
  }

Надеюсь, что это поможет.

5

Я следил за подходом @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 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);
    }
}
  • 0
    Это прекрасно работает. Это сохраняет сеанс между запросами до тех пор, пока не истекло время ожидания. Не уверен, собираюсь ли я использовать его в prod, пока не найду хороший способ переключить состояние сеанса между обязательным и только для чтения, чтобы остановить блокировку запросов, но это дало мне начальный путь, который я желаю. Спасибо!
1

нужно упомянуть об ответе @LachlanB.

protected void Application_PostAuthorizeRequest()
    {
        if (IsWebApiRequest())
        {
            HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required);
        }
    }

Если вы опустите строку if (IsWebApiRequest())

Весь сайт будет иметь проблему медленности загрузки страницы, если ваш сайт смешан с страницами веб-форм.

-5

Возвращаясь к основам, почему бы не сохранить его простым и сохранить значение 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

$(document).ready(function() {

    var sessionValue = $('#hBlah').val();

    alert(sessionValue);

    /* Now call your API with the session variable */}

}

  • 1
    Что если приложение использует как MVC, так и WebAPI? Кроме того, некоторые вещи более целесообразно хранить на стороне сервера, например токены безопасности Sharepoint. Вместо того, чтобы реализовывать специальную оболочку для хранения токенов, такую как контейнер Azure BLOB-объектов, иногда целесообразно повторно использовать Session для данных этого типа. Контекст безопасности Sharepoint, реализованный в шаблоне приложения, использует сеанс для хранения этих контекстов безопасности, и передаются только небольшие фрагменты данных (тег сеанса) вместо нескольких килобайт данных. Было бы здорово, если бы этот контекст был меньше ...

Ещё вопросы

Сообщество Overcoder
Наверх
Меню