使用ASP.NET Web API访问会话

Mar*_*ark 261 asp.net asp.net-web-api

我意识到会话和REST并不完全齐头并进,但是使用新的Web API无法访问会话状态吗?HttpContext.Current.Session永远是空的.

Roc*_*lan 328

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 }
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

这个解决方案还有额外的好处,我们可以在javascript中获取基本URL来进行AJAX调用:

_Layout.cshtml

<body>
    @RenderBody()

    <script type="text/javascript">
        var apiBaseUrl = '@Url.Content(ProjectNameSpace.WebApiConfig.UrlPrefixRelative)';
    </script>

    @RenderSection("scripts", required: false) 
Run Code Online (Sandbox Code Playgroud)

然后在我们的Javascript文件/代码中,我们可以进行可以访问会话的webapi调用:

$.getJSON(apiBaseUrl + '/MyApi')
   .done(function (data) {
       alert('session data received: ' + data.whatever);
   })
);
Run Code Online (Sandbox Code Playgroud)

的WebForms

执行上述操作但更改WebApiConfig.Register函数以取代RouteCollection:

public static void Register(RouteCollection routes)
{
    routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: WebApiConfig.UrlPrefix + "/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );
}
Run Code Online (Sandbox Code Playgroud)

然后在Application_Start中调用以下内容:

WebApiConfig.Register(RouteTable.Routes);
Run Code Online (Sandbox Code Playgroud)

Dot Net Core

添加Microsoft.AspNetCore.Session NuGet包,然后进行以下代码更改:

Startup.cs

在ConfigureServices函数中的services对象上调用AddDistributedMemoryCacheAddSession方法:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    ...

    services.AddDistributedMemoryCache();
    services.AddSession();
Run Code Online (Sandbox Code Playgroud)

并在Configure函数中添加对UseSession的调用:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
ILoggerFactory loggerFactory)
{
    app.UseSession();
    app.UseMvc();
Run Code Online (Sandbox Code Playgroud)

SessionController.cs

在控制器中,在顶部添加using语句:

using Microsoft.AspNetCore.Http;
Run Code Online (Sandbox Code Playgroud)

然后在代码中使用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);
    }
Run Code Online (Sandbox Code Playgroud)

你现在应该能够击中:

http://localhost:1234/api/session/set/thisissomedata
Run Code Online (Sandbox Code Playgroud)

然后转到此URL将其拉出:

http://localhost:1234/api/session/get
Run Code Online (Sandbox Code Playgroud)

有关在dot net core中访问会话数据的更多信息,请访问:https://docs.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

阅读微软关于避免跨站点请求伪造黑客攻击的文章.(简而言之,请查看AntiForgery.Validate方法) - https://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-csrf-attacks

  • 关于此修复,有一点需要提及.当将SessionStateBehavior设置为Required时,您就是瓶颈webapi,因为会话对象上的锁定会使您的所有请求同步运行.您可以将其作为SessionStateBehavior.Readonly运行.这样它就不会在会话对象上创建锁. (7认同)
  • 完善.简单而且有效.对于非MVC,只需将Application_PostAuthorizeRequest()添加到Global.ascx.cs即可. (6认同)
  • 我需要修改IsWebApiRequest()以返回true,其中路径以WebApiConfig.UrlPrefix开头,以及WebApiConfig.UrlPrefixRelative.除此之外,按预期工作. (3认同)
  • 将会话状态行为设置为"必需"时要小心.具有写入权限的请求将锁定会话并阻止每个客户端生成多个HttpApplication.您应该将会话状态设置为每个路由的适当级别.请参考我的回答:http://stackoverflow.com/a/34727708/1412787 (2认同)

war*_*ckh 65

您可以使用自定义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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在此处找到:http://techhasnoboundary.blogspot.com/2012/03/mvc-4-web-api-access-session.html

  • 更新:如果您的API函数从会话中读取,并且不修改会话,那么使用IReadOnlySessionState而不是IRequiresSessionState可能是个好主意.这可确保在处理API函数期间不会锁定会话. (13认同)
  • 在MVC 4中没有为我工作 - route.RouteHandler甚至不是我的财产.@LachlanB似乎对我有用. (6认同)
  • 感谢@bkwdesign指出MVC解决方案.此答案仅涉及Web API. (3认同)
  • 这似乎不支持路由属性.思考? (2认同)

Sim*_*ver 46

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

性能,性能,性能!

有一个非常好的,经常被忽视的原因,你根本不应该在WebAPI中使用Session.

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

并发请求和会话状态

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

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

所以你应该使用一个MVC控制器,如果你绝对需要来自用户会话的东西,并避免为WebApi启用它的不必要的性能损失.

您只需Thread.Sleep(5000)输入WebAPI方法并启用Session 即可轻松自行测试.运行5个请求,总共需要25秒才能完成.没有Session,他们总共需要5秒钟.

(同样的推理适用于SignalR).

  • 如果您的方法只从会话中读取,则可以使用[SessionState(SessionStateBehavior.ReadOnly)]来解决此问题. (17认同)

Nic*_*ckz 21

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

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

如果您需要会话忘记休息,则会话与基于REST的设计无关.如果您需要会话进行验证,请使用令牌或通过IP地址进行授权.

  • 标记你,因为你没有提供他的问题的答案,更是如此,Web Api是一个异步框架,适用于ajax沉重的Web应用程序.没有人说你必须尊重RESTful设计的所有内容才能从使用Web API框架中获益. (57认同)
  • 我不确定这个.在Microsoft的示例中,它们使用Authorize属性显示.我试过了,它适用于基于表单的身份验证.Web API了解在默认身份验证cookie中传递的身份验证状态. (10认同)
  • 以下是我所指的示例,http://code.msdn.microsoft.com/ASPNET-Web-API-JavaScript-d0d64dd7.它使用新的基于REST的Web API实现Forms身份验证. (4认同)
  • 我已成功使用[Authorize]属性而不需要会话状态.我刚刚写了一个身份验证消息处理程序来设置身份. (4认同)
  • @分数.是告知Web API不应该知道会话状态的权利.否定答案仍然是一个答案.投票. (3认同)

JSa*_*cho 20

Mark,如果你检查一下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
Run Code Online (Sandbox Code Playgroud)

你必须定义你的"SampleIdentity"类,你可以从nerddinner项目中借用它.


小智 13

要解决此问题:

protected void Application_PostAuthorizeRequest()
{
    System.Web.HttpContext.Current.SetSessionStateBehavior(System.Web.SessionState.SessionStateBehavior.Required);
}
Run Code Online (Sandbox Code Playgroud)

在Global.asax.cs中

  • 警告!这将启用所有请求的会话.如果您的应用程序使用嵌入式资源,这可能会损害性能. (4认同)

Cru*_*KID 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 
        );
Run Code Online (Sandbox Code Playgroud)

Global.asax中

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

private static bool IsWebApiRequest()
{
  return HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath.StartsWith(_WebApiExecutionPath);
}
Run Code Online (Sandbox Code Playgroud)

在这里:http://forums.asp.net/t/1773026.aspx/1


Stu*_*lor 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);
    }
Run Code Online (Sandbox Code Playgroud)


Tre*_*hor 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;
    }
Run Code Online (Sandbox Code Playgroud)

然后在你的api电话中你想要访问你刚刚做的会话:

HttpContextWrapper httpContextWrapper = GetHttpContextWrapper();
var someVariableFromSession = httpContextWrapper.Session["SomeSessionValue"];
Run Code Online (Sandbox Code Playgroud)

我也在我的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);
  }
}
Run Code Online (Sandbox Code Playgroud)

您也可以创建一个自定义过滤器属性,您可以将其粘贴到您需要会话的api调用上,然后您可以像通常通过HttpContext.Current.Session ["SomeValue"]那样在api调用中使用session:

  /// <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;
        }
      }
    }
  }
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助.


JCa*_*ico 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);
    }
}
Run Code Online (Sandbox Code Playgroud)