使用ASP.NET Web API访问会话

时间:2012-03-07 00:49:43

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

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

13 个答案:

答案 0 :(得分:313)

  

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

  

的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

在ConfigureServices函数中的服务对象上调用 AddDistributedMemoryCache AddSession 方法:

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

    services.AddDistributedMemoryCache();
    services.AddSession();

并在配置功能中添加对 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对并发请求强制执行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

答案 1 :(得分:63)

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

答案 2 :(得分:44)

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

性能,性能,性能!

为什么你根本不应该在WebAPI中使用Session,这是一个非常好的,经常被忽视的原因。

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

  

Concurrent Requests and Session State

     

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

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

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

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

(同样的理由适用于SignalR)。

答案 3 :(得分:21)

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

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

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

答案 4 :(得分:20)

马克,如果你检查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借用它。

答案 5 :(得分:12)

解决问题:

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

在Global.asax.cs

答案 6 :(得分: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

答案 7 :(得分: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 :(得分: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调用中使用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;
        }
      }
    }
  }

希望这有帮助。

答案 9 :(得分: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);
    }
}

答案 10 :(得分:3)

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

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

如果省略第if (IsWebApiRequest())

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

答案 11 :(得分:0)

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

https://jwt.io/

答案 12 :(得分:-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

$(document).ready(function(){

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

    alert(sessionValue);

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

}