使用ASP.NET Web API访问会话


268

我意识到会话和REST并不能完全并行,但是使用新的Web API是否无法访问会话状态?HttpContext.Current.Session始终为null。


4
[SessionState(SessionStateBehavior.Required)]ApiController技巧上(或.ReadOnly在适当的地方)。
罗曼·斯塔科夫

@RomanStarkov无法使它正常工作。您正在使用什么环境?.NET Core?
Bondolin '19

@Bondolin不,这不是Core。
罗曼·斯塔科夫

@RomanStarkov MVC呢?我找不到它。
Bondolin

@Bondolin SessionStateAttribute,是的,MVC。
罗曼·斯塔科夫

Answers:


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

}

此解决方案具有额外的好处,我们可以在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);
   })
);

Web表格

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

然后转到该URL将其拔出:

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

有关在点网核心内访问会话数据的更多信息,请访问:https : //docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state

性能问题

阅读下面有关性能的Simon Weaver的答案。如果您在WebApi项目中访问会话数据,则可能会导致非常严重的性能后果-我已经看到ASP.NET对并发请求强制执行200ms的延迟。如果您有许多并发请求,这可能加起来并造成灾难性的后果。


安全问题

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

阅读Microsoft关于ASP.NET Web API中的身份验证和授权的文章- //www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api

阅读Microsoft关于避免跨站点请求伪造hack攻击的文章。(总之,检查出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'4

1
感谢@JCallico,我想大多数人会首先访问ASP.NET页面,该页面创建了会话。
罗克兰2014年

3
我需要修改IsWebApiRequest()以在路径以WebApiConfig.UrlPrefix以及WebApiConfig.UrlPrefixRelative开头的地方也返回true。除此之外,按预期方式工作。
gb2d

7
关于此修复程序,要提一件事。当将SessionStateBehavior设置为Required时,您将成为webapi的瓶颈,因为由于会话对象上的锁定,所有请求都将同步运行。您可以改为将其作为SessionStateBehavior.Readonly运行。这样,它将不会在会话对象上创建锁。
Michael Kire Hansen

2
将会话状态行为设置为“必需”时要小心。具有写许可权的请求将锁定会话,并防止每个客户端产生多个HttpApplication。您应将每个路由的会话状态设置为适当的级别。请在这里参考我的答案:stackoverflow.com/a/34727708/1412787
Axel Wilczek'1

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

6
在MVC 4中对我不起作用-route.RouteHandler甚至对我来说都不是属性。@LachlanB似乎对我有用。
bkwdesign 2013年

3
感谢@bkwdesign指出MVC解决方案。该答案仅与Web API有关。
warrickh

2
这似乎不支持路由属性。有什么想法吗?
Tim S

正如bkwdesign指出的那样,不再支持此功能。但是,有一种方法可以使用DataTokens定义每个路由的会话状态行为:stackoverflow.com/a/34727708/1412787
Axel Wilczek,

46

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

性能,性能,性能!

为什么根本不应该在WebAPI中使用Session有一个很好的且经常被忽略的原因。

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

并发请求和会话状态

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

那么这对Web 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属性。我已经尝试过了,它可以与基于表单的身份验证一起使用。Web API知道在默认身份验证cookie中传递的身份验证状态。
2012年

4
这是我指的示例code.msdn.microsoft.com/ASPNET-Web-API-JavaScript-d0d64dd7。它使用新的基于REST的Web API实施表单身份验证。
2012年

4
我已经成功使用[Authorize]属性,而无需会话状态。我只是写了一个身份验证消息处理程序来设置身份。
安东尼·斯科特

57
因为您没有提供有关他问题的答案而使您失望,更重要的是,Web Api是一个异步框架,与ajax繁重的Web应用程序配合使用非常出色。没有人说您必须尊重RESTful设计的所有原则,才能从使用Web API框架中受益。
Brian Ogden

3
@分数。通知Web API不应该知道会话状态是正确的。否定答案仍然是答案。投票。
Antoine Meltzheim 2014年

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

这对我不起作用(在.NET 4中)。我从来没有那个饼干。仅当您打开FormsAuthentication时才起作用吗?
mhenry1384'4

通过登录表单进行身份验证后,确实会生成Cookie。您还可以自定义创建方式/创建时间,请参见stackoverflow.com/questions/7217105,但是您仍然需要用户针对Web服务器进行有效身份验证
JSancho 2014年

该问题要求使用HttpContext.Current.Session,而此答案并未明确说明需要做什么。参见@LachlanB答案。
JCallico 2014年

14

要解决此问题:

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

在Global.asax.cs中


4
警告!这将为所有请求启用会话。如果您的应用程序正在使用嵌入式资源,则这确实会损害性能。
cgatian

@cgatian任何替代解决方案固定
Kiquenet '16

我认为最好的方法是@Treyphor提出的建议。不要为所有请求启用它。仅在URL中包含“ / api”或其他内容的路由。另外,如果可能,请将会话状态设置为仅对您的API控制器读取。
cgatian

10

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

在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


这是最简单的解决方案,但是在代码中存在一些错误,因此实际上不起作用。我已经发布了基于此解决方案的另一种解决方案,请随时根据我的要求进行编辑。
罗克兰

_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调用上,然后可以像通常通过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;
        }
      }
    }
  }

希望这可以帮助。


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

这很好。只要会话没有超时,这将使会话在请求之间保持不变。不知道我是否要在产品中使用它,直到我找到了一种在必需和只读之间切换会话状态以停止请求阻塞的好方法,但这为我提供了我想要的起始路径。谢谢!
Derreck Dean 2016年

3

需要在@LachlanB的答案中提及一件事。

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

如果您省略行 if (IsWebApiRequest())

如果您的网站与Web表单页面混合在一起,则整个网站都会出现页面加载缓慢的问题。


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

Java脚本

$(document).ready(function(){

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

    alert(sessionValue);

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

}


1
如果应用程序同时使用MVC和WebAPI,该怎么办?同样,将某些东西存储在服务器端是更合理的,例如,Sharepoint安全令牌。代替为像azure blobs容器之类的令牌存储实现特殊的包装,有时是针对此类数据的合理重用Session。在应用程序模板中实现的Sharepoint安全上下文使用会话存储这些安全上下文,并且仅传输一小部分数据(会话标签),而不是几千字节的数据。如果这些内容较小,那将是非常棒的……
康斯坦丁·伊萨夫2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.