Доступ к сеансу с использованием ASP.NET Web API


268

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


4
[SessionState(SessionStateBehavior.Required)]на ApiControllerделает трюк (или, .ReadOnlyгде это уместно).
Роман Старков

@RomanStarkov Не удалось заставить это работать. Какую среду вы использовали? .NET Core?
Бондолин

@ Бондолин нет, это был не Core.
Роман Старков

@RomanStarkov MVC тогда? У меня проблемы с поиском.
Бондолин

Ответы:


336

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

Dot Net Core

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

Startup.cs

Вызовите методы 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();

SessionController.cs

В вашем контроллере добавьте оператор использования вверху:

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


7
Отлично. Все просто и работает. Для не MVC просто добавьте Application_PostAuthorizeRequest () в Global.ascx.cs.
mhenry1384

1
Спасибо @JCallico, я думаю, что большинство людей сначала заходят на страницу ASP.NET, которая создает сеанс
Роклан

3
Мне нужно было изменить функцию IsWebApiRequest (), чтобы она также возвращала значение true, если путь начинается с WebApiConfig.UrlPrefix, а также WebApiConfig.UrlPrefixRelative. Кроме того, работает как ожидалось.
gb2d

7
Одна вещь, чтобы упомянуть относительно этого исправления. при установке значения SessionStateBehavior в значение Required вы ставите узкое место в webapi, поскольку все ваши запросы будут синхронизированы из-за блокировок объекта сеанса. Вместо этого вы можете запустить его как SessionStateBehavior.Readonly. Таким образом, он не будет создавать блокировки на объекте сеанса.
Майкл Кире Хансен

2
Будьте внимательны при установке поведения сеанса в состояние «Обязательно». Запросы с разрешениями на запись блокируют сеанс и предотвращают создание нескольких приложений Http для каждого клиента. Вы должны установить состояние сеанса на соответствующий уровень для каждого маршрута. Пожалуйста, обратитесь к моему ответу здесь: stackoverflow.com/a/34727708/1412787
Аксель Вильчек

66

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


14
Обновление: если ваши функции API читают из сеанса и не изменяют сеанс, было бы неплохо использовать IReadOnlySessionState вместо IRequiresSessionState. Это гарантирует, что сеанс не заблокирован во время обработки функции API.
Warrickh

6
не работает для меня в MVC 4 - route.RouteHandler даже не собственность для меня. @LachlanB, похоже, работает на меня.
bkwdesign

3
Спасибо @bkwdesign за указание решения MVC. Этот ответ относится только к веб-API.
Warrickh

2
Кажется, это не поддерживает атрибуты маршрута. Мысли?
Тим С

Как указал bkwdesign, это больше не поддерживается. Тем не менее, есть способ определить поведение состояния сеанса для каждого маршрута с помощью DataTokens: stackoverflow.com/a/34727708/1412787
Аксель Вильчек,

46

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

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

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

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

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

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

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

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

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

(То же самое относится и к SignalR).


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

21

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

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

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


10
Я не уверен в этом. В примерах Microsoft они показывают использование атрибута Authorize. Я пробовал это, и это работает с проверкой подлинности на основе форм. Веб-API знает о состоянии аутентификации, которое передается в куки-файле аутентификации по умолчанию.
Марк

4
Вот пример, на который я ссылаюсь: code.msdn.microsoft.com/ASPNET-Web-API-JavaScript-d0d64dd7 . Он использует новый веб-API на основе REST, реализующий проверку подлинности с помощью форм.
Марк

4
Я успешно использовал атрибут [Authorize], не нуждаясь в состоянии сеанса. Я просто написал обработчик сообщения аутентификации, чтобы установить личность.
Энтони Скотт

57
Вы заметили, потому что вы не предложили ответ на его проблему, и, более того, Web Api - это асинхронный фреймворк, который отлично работает с тяжелым веб-приложением ajax. Никто не говорил, что вы должны уважать все принципы дизайна RESTful, чтобы получить выгоду от использования инфраструктуры Web API.
Брайан Огден

3
@Метки. Правильно сообщить, что Web API не должен знать о состоянии сеанса. Отрицательный ответ до сих пор остается ответом. До голосования.
Антуан Мельцхайм

20

Марк, если вы посмотрите на пример 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 .


Класс идентификации находится в NerdDinner_2.0 \ NerdDinner \ Models \ NerdIdentity.cs.
mhenry1384

Это не работает для меня (в .NET 4). У меня никогда не было этого печенья. Работает ли это только если у вас включена FormsAuthentication?
mhenry1384

cookie действительно генерируется после аутентификации через форму входа. Вы также можете настроить, как / когда он будет создан, см. Stackoverflow.com/questions/7217105 Но вам все еще нужен пользователь для эффективной аутентификации на веб-сервере
JSancho

Вопрос требует HttpContext.Current.Session, и этот ответ не дает четкого объяснения того, что необходимо сделать. Смотрите ответ @LachlanB.
JCallico

14

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

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

в Global.asax.cs


4
Предупреждение! Это включит сеанс для ВСЕХ запросов. Это действительно может снизить производительность, если ваше приложение использует встроенные ресурсы.
cgatian

@cgatian какое-нибудь альтернативное решение исправлено ?
Kiquenet

Я думаю, что лучший подход - это то, что предлагает @Treyphor. Не включайте его для всех запросов. Просто маршруты, которые имеют "/ api" или что-то в URL. Также, если возможно, установите состояние сеанса только для чтения для ваших контроллеров API.
cgatian

10

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

в 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


Это самое простое решение, но в коде есть несколько ошибок, так что оно на самом деле не работает. Я опубликовал другое решение, основанное на этом, не стесняйтесь редактировать свое, чтобы оно соответствовало моему.
Роклан

Небольшое исправление в строке _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);
    }

8

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

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


6

Я следовал подходу @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);
    }
}

Это прекрасно работает. Это сохраняет сеанс между запросами до тех пор, пока не истекло время ожидания. Не уверен, собираюсь ли я использовать его в prod, пока не найду хороший способ переключить состояние сеанса между обязательным и только для чтения, чтобы остановить блокировку запросов, но это дало мне начальный путь, который я хочу. Спасибо!
Дерек Дин

3

В ответе @LachlanB нужно упомянуть одну вещь.

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

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

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


0

Да, сессия не идет рука об руку с Rest API, и мы также должны избегать этой практики. Но в соответствии с требованиями нам нужно как-то поддерживать сеанс так, чтобы в каждом запросе клиентский сервер мог обмениваться или поддерживать состояние или данные. Таким образом, лучший способ достичь этого без нарушения протоколов REST - это общение через токен, такой как JWT.

https://jwt.io/


-4

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

}


1
Что если приложение использует MVC и WebAPI? Кроме того, некоторые вещи более целесообразно хранить на стороне сервера, например токены безопасности Sharepoint. Вместо того, чтобы реализовывать специальную оболочку для хранения токенов, например контейнер BLOB-объектов Azure, иногда целесообразно повторно использовать Session для данных этого типа. Контекст безопасности Sharepoint, реализованный в шаблоне приложения, использует сеанс для хранения этих контекстов безопасности и передает только небольшие фрагменты данных (тег сеанса) вместо нескольких килобайт данных. Было бы замечательно, если бы этот контекст был меньше ...
Константин Исаев
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.