使用ASP.NET Web API访问会话

297

我知道Session和REST不太相关,但使用新的Web API访问会话状态是否不可能?HttpContext.Current.Session始终为空。


9
ApiController 上使用 [SessionState(SessionStateBehavior.Required)](或在适当的情况下使用 .ReadOnly)可以解决这个问题。 - Roman Starkov
@RomanStarkov 无法让它正常工作。你使用的是什么开发环境?.NET Core? - Bondolin
1
@Bondolin 不,这不是核心。 - Roman Starkov
@Bondolin SessionStateAttribute,是的,这是MVC相关的内容。 - Roman Starkov
1
@RomanStarkov SessionState 只适用于 MVC 控制器,不适用于 Web Api 控制器。 - boggy
显示剩余2条评论
13个回答

366

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 文件/代码中,我们可以进行 Web API 调用,访问会话:

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












Add the Microsoft.AspNetCore.Session NuGet package and then call the AddDistributedMemoryCache and AddSession methods on the services object within the ConfigureServices function in Startup.cs:

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 core中访问会话数据的更多信息,请参阅此处:https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state

有关性能问题,请查看Simon Weaver在下面的回答。如果您在WebApi项目中访问会话数据,可能会导致非常严重的性能后果 - 我曾经看到ASP.NET对并发请求强制执行200ms的延迟。如果您有许多并发请求,这可能会累加并变得灾难性。


确保根据用户锁定资源 - 已验证的用户不应该能够从您的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
3
我需要修改IsWebApiRequest()的代码,让它在路径以WebApiConfig.UrlPrefix或WebApiConfig.UrlPrefixRelative开头时也返回true。除此之外,一切都如预期的那样运行。 - gbro3n
7
关于这个修正需要提到一点,当将SessionStateBehavior设置为Required时,会造成WebAPI的瓶颈,因为由于对session对象的锁定,所有请求都将同步运行。您可以选择将其设置为SessionStateBehavior.ReadOnly,这样就不会在session对象上创建锁定。 - Michael Kire Hansen
2
在设置会话状态行为为“Required”时要小心。具有写权限的请求将锁定会话并防止每个客户端生成多个HttpApplications。 您应该为每个路由设置适当的会话状态级别。 请参考我的答案:https://dev59.com/rYnca4cB1Zd3GeqP9FDp#34727708 - Axel Wilczek
1
大家好,我该如何在我的Dot net Core上设置[SessionState(SessionStateBehavior.ReadOnly)]? - Paula Fleck
显示剩余13条评论

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解决方案。这个答案只涉及Web API。 - warrickh
2
这似乎不支持路由属性。你有什么想法? - Tim S
正如bkwdesign所指出的那样,这已不再受支持。但是,可以使用DataTokens为每个路由定义会话状态行为:https://dev59.com/rYnca4cB1Zd3GeqP9FDp#34727708 - Axel Wilczek
显示剩余2条评论

52

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

性能、性能、性能!

有一个很好的理由,通常被忽视了,这就是为什么你根本不应该在WebAPI中使用Session。

当Session被使用时,ASP.NET会序列化来自单个客户端的所有请求。我不是指对象序列化,而是按收到顺序运行它们,并等待每个请求完成后再运行下一个。这是为了避免两个请求同时尝试访问Session时出现丑陋的线程/竞态条件。

并发请求和会话状态

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

那么这对Web API意味着什么呢?如果你有一个运行多个AJAX请求的应用程序,那么只有一个请求能够同时运行。如果你有一个较慢的请求,那么它将阻塞来自该客户端的所有其他请求,直到该请求完成。在某些应用程序中,这可能会导致非常明显的性能下降。

因此,如果你确实需要从用户会话中获取一些内容,则应使用MVC控制器,并避免为WebAPI启用Session带来的不必要的性能惩罚。

您可以轻松地通过在WebAPI方法中加入Thread.Sleep(5000)并启用Session来自行测试。向其发送5个请求,它们将总共花费25秒钟才能完成。如果没有启用Session,则它们将只需稍微超过5秒钟即可完成。(此相同的推理适用于SignalR)。

21
如果您的方法只从session中读取数据,您可以使用 [SessionState(SessionStateBehavior.ReadOnly)] 来解决此问题。 - Rocklan
对于您提供的5个并发请求的场景,我会在第一个请求上阻止UI,并且不允许它们创建多个请求,直到最后一个结束。如果我不使用会话密钥,在为移动应用程序创建Web API时,我将如何验证用户身份? - Ali123

22

没错,REST是无状态的。如果使用会话,则处理将变为有状态,随后的请求将能够使用状态(来自会话)。

为了重新激活会话,您需要提供一个与状态相关联的键。在普通的asp.net应用程序中,通过使用cookie(cookie会话)或url参数(无cookie会话)来提供该键。

如果您需要会话,则忘记REST,在基于REST的设计中,会话不相关。如果您需要会话以进行验证,那么请使用令牌或按IP地址授权。


10
我不确定这件事。在微软的示例中,他们展示了如何使用Authorize属性。我尝试过并且它适用于基于表单的身份验证。Web API知道正在传递默认身份验证cookie中的身份验证状态。 - Mark
4
这是我参考的样例,http://code.msdn.microsoft.com/ASPNET-Web-API-JavaScript-d0d64dd7。它使用基于新REST的Web API实现表单认证。 - Mark
5
我已成功地使用了 [Authorize] 属性,而不需要会话状态。我只需编写一个身份验证消息处理程序来设置身份。 - Antony Scott
58
因为你没有提供答案,所以有人给你打了分数低的评价。此外,Web Api是一个异步框架,非常适合与ajax-heavy的web应用程序一起使用。并不是说你必须遵循RESTful设计的所有原则才能从使用Web API框架中获得好处。 - Brian Ogden
4
马克正确地指出Web API不应该知道会话状态。否定的答案仍然是一个答案。点赞。 - Antoine Meltzheim
显示剩余5条评论

20

如果你查看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项目中借鉴。


身份类位于NerdDinner_2.0\NerdDinner\Models\NerdIdentity.cs。 - mhenry1384
这对我不起作用(在.NET 4中)。我从来没有那个 cookie。它只有在您开启了 FormsAuthentication 情况下才能工作吗? - mhenry1384
在通过登录表单进行身份验证后确实会生成Cookie。您还可以自定义生成Cookie的方式/时间,请参见https://dev59.com/OGw05IYBdhLWcg3weBzv 但仍然需要用户有效地对Web服务器进行身份验证。 - JSancho
该问题要求访问HttpContext.Current.Session,而这个答案并没有清楚地解释需要做什么。请查看@LachlanB的答案。 - JCallico

16

修复问题的方法:

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

在 Global.asax.cs 文件中


4
警告!这将为所有请求启用会话。如果您的应用程序使用了嵌入式资源,这可能会严重影响性能。 - cgatian
@cgatian,有没有其他的解决方案已经修复了? - Kiquenet
我认为最好的方法是@Treyphor所建议的。不要为所有请求启用它,只针对URL中包含"/api"或类似内容的路由。此外,如果可能的话,将会话状态设置为只读模式适用于你的API控制器。 - cgatian

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

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


这是最简单的解决方案,但代码中有一些错误,因此它实际上无法正常工作。我已发布了另一个基于此的解决方案,随意编辑您的代码以匹配我的。 - Rocklan
稍作更正,_WebApiExecutionPath这一行需要改为 public static string _WebApiExecutionPath = "~/api";。 - stephen ebichondo

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 调用上使用它,然后您可以像通常一样通过 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;
        }
      }
    }
  }

希望这可以帮到您。

抱歉,您在基本APIController中创建的GetHttpContextWrapper()方法。但是Base ApiContorller类是抽象类。您是如何实现它的? - meekash55

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

这个很好用。只要会话没有超时,它就可以在请求之间保持相同的会话。我还不确定是否会在生产环境中使用它,直到我找到一种很好的方法来在必需和只读之间切换会话状态以防止请求阻塞,但是这给了我所需要的起点。谢谢! - Derreck Dean

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接