【问题标题】:Accessing Session Using ASP.NET Web API使用 ASP.NET Web API 访问会话
【发布时间】:2012-03-24 13:16:16
【问题描述】:

我意识到 session 和 REST 并不完全一致,但是使用新的 Web API 访问 session 状态是不可能的吗? HttpContext.Current.Session 始终为空。

【问题讨论】:

  • [SessionState(SessionStateBehavior.Required)] ApiController 可以解决问题(或 .ReadOnly 在适当的情况下)。
  • @RomanStarkov 无法正常工作。你用的是什么环境? .NET 核心?
  • @Bondolin 不,这不是核心。
  • @RomanStarkov MVC 然后呢?我找不到它。
  • @Bondolin SessionStateAttribute 是的,MVC。

标签: asp.net asp.net-web-api


【解决方案1】:

是的,会话与 Rest API 并不一致,我们也应该避免这种做法。但是根据要求,我们需要以某种方式维护会话,以便在每个请求中客户端服务器都可以交换或维护状态或数据。因此,在不破坏 REST 协议的情况下实现这一目标的最佳方式是通过 JWT 等令牌进行通信。

https://jwt.io/

【讨论】:

    【解决方案2】:

    为什么要避免在 WebAPI 中使用 Session?

    性能、性能、性能!

    您根本不应该在 WebAPI 中使用 Session 有一个很好但经常被忽视的原因。

    使用 Session 时 ASP.NET 的工作方式是序列化从单个客户端接收到的所有请求。现在我不是在谈论对象序列化 - 而是按照收到的顺序运行它们并等待每个完成,然后再运行下一个。这是为了避免两个请求同时尝试访问 Session 时出现讨厌的线程/竞争条件。

    Concurrent Requests and Session State

    访问 ASP.NET 会话状态 每个会话都是独占的,这意味着如果两个不同的用户 并发请求,授予对每个单独会话的访问权限 同时。但是,如果对 相同的会话(通过使用相同的 SessionID 值),第一个请求 获得对会话信息的独占访问权。第二个请求 仅在第一个请求完成后执行。(第二个会话 如果信息的排他锁被释放,也可以获得访问权限 因为第一个请求超过了锁定超时。)如果 @Page 指令中的 EnableSessionState 值设置为 ReadOnly,一个 对只读会话信息的请求不会导致 会话数据的排他锁。但是,只读请求 会话数据可能仍然需要等待读写设置的锁 请求清除会话数据。

    那么这对 Web API 意味着什么?如果您有一个应用程序运行许多 AJAX 请求,那么一次只能运行一个。如果您的请求较慢,那么它将阻止该客户端的所有其他请求,直到完成。在某些应用程序中,这可能会导致性能非常缓慢。

    因此,如果您绝对需要用户会话中的某些内容,并且避免为 WebApi 启用它而导致不必要的性能损失,那么您可能应该使用 MVC 控制器。

    您只需将 Thread.Sleep(5000) 放入 WebAPI 方法并启用 Session 即可轻松地自行测试。对它运行 5 个请求,它们总共需要 25 秒才能完成。如果没有 Session,它们总共需要 5 秒多一点。

    (同样的推理也适用于 SignalR)。

    【讨论】:

    • 如果您的方法仅从会话中读取,您可以使用 [SessionState(SessionStateBehavior.ReadOnly)] 来解决此问题。
    • 对于您的 5 个并发请求的场景,我在第一个请求上阻止 UI,并且不允许他们创建多个请求,直到最后一个请求结束。如果我不使用会话密钥,如果我为移动应用创建 Web api,我将如何验证用户身份?
    【解决方案3】:

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

    这个解决方案还有一个额外的好处是我们可以在 javascript 中获取基本 URL 以进行 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);
       })
    );
    

    网络表单

    执行上述操作,但将 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

    在 ConfigureServices 函数中调用服务对象的 AddDistributedMemoryCacheAddSession 方法:

    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
    

    然后去这个网址就会把它拉出来:

    http://localhost:1234/api/session/get
    

    更多关于在 dot net core 中访问会话数据的信息:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state

    性能问题

    阅读下面关于性能的 Simon Weaver 的回答。如果您在 WebApi 项目中访问会话数据,它可能会对性能产生非常严重的影响——我已经看到 ASP.NET 对并发请求强制执行 200 毫秒的延迟。如果您有许多并发请求,这可能会加起来并成为灾难性的。


    安全问题

    确保您锁定了每个用户的资源 - 经过身份验证的用户不应该能够从您的 WebApi 检索他们无权访问的数据。

    阅读微软关于 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

    【讨论】:

    • 完美。简单而且有效。对于非 MVC,只需将 Application_PostAuthorizeRequest() 添加到 Global.ascx.cs。
    • 感谢@JCallico,我猜大多数人会首先访问创建会话的 ASP.NET 页面。
    • 我需要修改 IsWebApiRequest() 以在路径以 WebApiConfig.UrlPrefix 以及 WebApiConfig.UrlPrefixRelative 开头的位置也返回 true。除此之外,按预期工作。
    • 关于此修复需要提及的一件事。将 SessionStateBehavior 设置为必需时,您会遇到 webapi 的瓶颈,因为由于会话对象上的锁定,您的所有请求都将同步运行。您可以改为将其作为 SessionStateBehavior.Readonly 运行。这样它就不会在会话对象上创建锁。
    • 将会话状态行为设置为“必需”时要小心。具有写入权限的请求将锁定会话并防止每个客户端生成多个 HttpApplications。您应该将会话状态设置为每个路由的适当级别。请在此处参考我的回答:stackoverflow.com/a/34727708/1412787
    【解决方案4】:

    @LachlanB 的回答需要提及一件事。

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

    如果你省略了if (IsWebApiRequest())这一行

    如果您的网站与网页表单页面混合,整个网站将出现页面加载缓慢的问题。

    【讨论】:

      【解决方案5】:

      解决问题:

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

      在 Global.asax.cs 中

      【讨论】:

      • 警告!这将为所有请求启用会话。如果您的应用程序使用嵌入式资源,这确实会损害性能。
      • @cgatian 任何替代解决方案已修复 ?
      • 我认为最好的方法是@Treyphor 建议的。不要为所有请求启用它。只是在 URL 中有“/api”或其他内容的路由。此外,如果可能,将会话状态设置为只读 API 控制器。
      【解决方案6】:

      我遵循@LachlanB 方法,当请求中存在会话 cookie 时,会话确实可用。缺少的部分是如何将 Session 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 中使用它,直到我找到一种在必需和只读之间切换会话状态以停止请求阻塞的好方法,但这给了我想要的起始路径。谢谢!
      【解决方案7】:

      我在 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 调用上,然后您可以像往常一样通过 HttpContext.Current.Session["SomeValue"] 在您的 api 调用中使用会话:

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

      希望这会有所帮助。

      【讨论】:

        【解决方案8】:

        回归基础,为什么不保持简单并将 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 */}
        

        }

        【讨论】:

        • 如果应用程序同时使用 MVC 和 WebAPI 怎么办?此外,将某些内容存储在服务器端更为合理,例如。 G。 Sharepoint 安全令牌。而不是像 azure blobs 容器那样为令牌存储实现一个特殊的包装器,有时对这种类型的数据重用 Session 是合理的。在应用程序模板中实现的 Sharepoint 安全上下文使用会话来存储这些安全上下文,并且只传输少量数据(会话标记)而不是几千字节的数据。如果这些上下文更小,那就太棒了……
        【解决方案9】:

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

        【讨论】:

          【解决方案10】:

          您可以使用自定义 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

          【讨论】:

          • 更新:如果您的 API 函数从会话中读取,并且不修改会话,那么使用 IReadOnlySessionState 而不是 IRequiresSessionState 可能是个好主意。这可确保在 API 函数处理期间会话不会被锁定。
          • 在 MVC 4 中不适合我 - route.RouteHandler 甚至不是我的属性。 @LachlanB 似乎对我有用。
          • 感谢 @bkwdesign 指出 MVC 解决方案。此答案仅与 Web API 有关。
          • 这似乎不支持路由属性。想法?
          • 正如 bkwdesign 指出的,这不再受支持。但是,有一种方法可以使用 DataTokens 定义每个路由的会话状态行为:stackoverflow.com/a/34727708/1412787
          【解决方案11】:

          最后一个现在不工作了,拿这个,它对我有用。

          在 App_Start 的 WebApiConfig.cs 中

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

          全球.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";
          【解决方案12】:

          马克,如果你检查nerddinner MVC example,逻辑几乎是一样的。

          您只需要检索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 project借用它。

          【讨论】:

          • 身份类在NerdDinner_2.0\NerdDinner\Models\NerdIdentity.cs。
          • 这对我不起作用(在 .NET 4 中)。我从来没有那个饼干。仅当您打开 FormsAuthentication 时才有效?
          • 在您通过登录表单进行身份验证后确实会生成 cookie。您还可以自定义创建方式/时间,请参阅 stackoverflow.com/questions/7217105 但您仍然需要用户有效地针对 Web 服务器进行身份验证
          • 这个问题要求 HttpContext.Current.Session 而这个答案并没有清楚地解释需要做什么。请参阅@LachlanB 答案。
          【解决方案13】:

          你说得对,REST 是无状态的。如果您使用会话,则处理将变为有状态,后续请求将能够使用状态(来自会话)。

          为了使会话重新水化,您需要提供一个键来关联状态。在普通的 asp.net 应用程序中,密钥是通过使用 cookie(cookie-sessions)或 url 参数(cookieless session)来提供的。

          如果您需要会话忘记休息,那么会话在基于 REST 的设计中是无关紧要的。如果您需要会话进行验证,请使用令牌或通过 IP 地址授权。

          【讨论】:

          • 对此我不确定。在 Microsoft 的示例中,它们显示了使用 Authorize 属性。我已经尝试过了,它适用于基于表单的身份验证。 Web API 知道在默认身份验证 cookie 中传递的身份验证状态。
          • 这是我所指的示例,code.msdn.microsoft.com/ASPNET-Web-API-JavaScript-d0d64dd7。它使用新的基于 REST 的 Web API 实现表单身份验证。
          • 我已经成功使用了 [Authorize] 属性,而无需会话状态。我刚刚编写了一个身份验证消息处理程序来设置身份。
          • 标记了你,因为你没有为他的问题提供答案,更重要的是,Web Api 是一个异步框架,非常适合 ajax 繁重的 web 应用程序。没有人说您必须尊重 RESTful 设计的所有原则才能从使用 Web API 框架中获益。
          • @MarkS。通知 Web API 不应该知道会话状态是正确的。否定的答案仍然是一个答案。赞成票。
          猜你喜欢
          • 1970-01-01
          • 2014-08-25
          • 2014-11-04
          • 2012-07-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-15
          • 2012-07-13
          相关资源
          最近更新 更多