Session (会话)

创建时间:
2014-02-22 01:35
最近更新:
2018-06-03 11:23

Breif

  • Session 的原理就是在 Cookie 里存储 ASP.NET_SessionId,随每次请求带去服务器进行身份验证。如果客户端禁用 Cookie,就把 ASP.NET_SessionId 保存在 url 里。
  • 目前有三种广泛使用的 在 web 环境中 维护会话 (传递 sessionid) 的方法: url 参数、隐藏域 和 cookie。其中每一种都各有利弊,cookie 已经被证明是三种方法中最方便最安全的。从安全的观点,如果不是全部也是绝大多数针对基于 cookie 的会话管理机制的攻击对于 url 或 隐藏域 机制同样适用,但是反过来却不一定,这就让 cookie 成为从安全考虑的最佳选择。

测试记录 (2018-05-30)

  • 生成新 SessionID 的原因: 如论多少次请求响应往返,只要 "代码中未曾 set Session" 且 "Global.asax.cs 文件中无 Session_Start/Session_End (即使是空方法)",则 Session.IsNewSession 始终为 trueSession.Count 始终为 0、始终不在响应头中 Set-Cookie (这导致始终为每次请求生成新的 Session.SessionID)。
  • 创建相关 Cookie 的原因: 如果请求中无 Session 相关 Cookie,则 "代码中 set Session" 或 "Global.asax.cs 文件中有 Session_Start/Session_End (即使是空方法)" 都会导致响应头中有 Set-Cookie: ASP.NET_SessionId=4bu4vmbqxv0swrsssh20qc1b; path=/; HttpOnly,客户端后续请求将带上此 Cookie,这导致后续响应头中不再有 Set-Cookie,因此此后 SessionID 不再改变。
  • Global.asax 中以下 4 种空方法均有效: void Session_Start() { }void Session_End() { }void Session_Start(object sender, EventArgs e) { }void Session_End(object sender, EventArgs e) { }
  • Session.SessionID 共 24 字符。例如: wsw40dmmujgvsnrgsos3q2ppvwu5ngpkyxcac0nlb0lic5mc2nfquovqrzu2jpbdl5ve4tloos2uc0puzhxbypd05xyjqija
  • Web.config 中的 <sessionState cookieName="NewName"></sessionState> 可将 浏览器中的 cookie 名 由 默认值 ASP.NET_SessionId 修改为 NewName
  • 从 IE 的 F12 的 cookie 中复制 ASP.NET_SessionId,在 FF 或 Chrome 的 F12 中重建该 cookie (例如 document.cookie="ASP.NET_SessionId=os2uc0puzhxbypd05xyjqija";),即可实现 会话劫持。
  • 标签之间共享 ASP.NET_SessionId 的 cookie 的浏览器有: FF, Chrome, IE。
  • 窗口之间共享 ASP.NET_SessionId 的 cookie 的浏览器有: FF, Chrome。
  • 窗口之间不共享 ASP.NET_SessionId 的 cookie 的浏览器有: IE。
  • /Home/Index 页面的后台代码中创建的 Cookie,其 path 默认为 /Home

Resource - MSDN

Part A

  1. ASP.NET 状态管理概述
  2. ASP.NET 会话状态概述
  3. 会话标识符 - 浏览器的会话使用存储在 SessionID 属性中的唯一标识符进行标识。会话 ID 使 ASP.NET 应用程序能够将特定的浏览器与 Web 服务器上相关的会话数据和信息相关联。会话 ID 的值在浏览器和 Web 服务器间通过 cookie 进行传输,如果指定了无 cookie 会话,则通过 URL 进行传输。
  4. 保护会话状态
  1. SessionStateModule 类 - public sealed class SessionStateModule : IHttpModule. 为应用程序提供会话状态服务。SessionStateModule 是 ASP.NET 默认的会话状态处理程序。它可以向会话状态存储写入和检索数据,及引发 Session_OnStart 和 Session_OnEnd 事件。
  2. SessionStateModule 成员 - Start & End 事件

Part B

以下内容摘自 sessionState 元素 (ASP.NET 设置架构).

The following sections describe Parent Elements, Attributes and Child Elements:

<configuration>
    <system.web>
        <sessionState
            allowCustomSqlDatabase="[True|False]"
            cookieless="[true|false|AutoDetect|UseCookies|UseUri|UseDeviceProfile]"
            cookieName="session identifier cookie name"
            customProvider="custom provider name"
            mode="[Off|InProc|StateServer|SQLServer|Custom]"
            partitionResolverType=""
            regenerateExpiredSessionId="[True|False]"
            sqlCommandTimeout="number of seconds"
            sqlConnectionString="sql connection string"
            stateConnectionString="tcpip=server:port"
            stateNetworkTimeout="number of seconds"
            timeout="number of minutes"
            useHostingIdentity="[True|False]"
            >
            <providers>...</providers>
        </sessionState>
    <system.web>
<configuration>

默认配置 - 下面的默认 <sessionState> 元素不是在 Machine.config 文件或根 Web.config 文件中显式配置的,而是由应用程序返回的默认配置。

<sessionState
    allowCustomSqlDatabase="false"
    cookieless="UseCookies"
    cookieName="ASP.NET_SessionId"
    customProvider=""
    mode="InProc"
    partitionResolverType=""
    regenerateExpiredSessionId="true"
    sqlCommandTimeout="30"
    sqlConnectionString="data source=127.0.0.1;Integrated Security=SSPI"
    stateConnectionString="tcpip=127.0.0.1:42424"
    stateNetworkTimeout="10"
    timeout="20"
    useHostingIdentity="true"
    >
    <providers>
        <clear />
    </providers>
</sessionState>

Resource - MSDN - Global.asax 文件中的 Session_StartSession_End

  1. 会话状态事件 - ASP.NET 提供了两种帮助您管理用户会话的事件: Session_OnStart 事件和 Session_OnEnd 事件; 前者在新会话开始时引发,后者在会话被放弃或过期时引发。
  2. SessionStateModule.Start 事件 - 创建会话时发生的事件。启动新会话后,在请求的初始阶段引发了 Start 事件。如果发送的请求未包含会话标识符、会话标识符无效或与会话标识符关联的会话已过期,则会启动新的会话。Session_OnStart 事件用于执行会话的任何初始化工作,例如设置会话变量的默认值。您可以通过向 Global.asax 文件添加一个名为 Session_OnStart 的公共子例程,为 Start 事件指定一个处理程序。
  3. SessionStateModule.End 事件 - 在会话结束时发生。如果已调用 Abandon 方法或会话已过期,End 事件将在请求结束时引发。如果在 Timeout 属性指定的分钟数范围内未向会话发送请求,会话将会过期。Session_OnEnd 事件用于对会话执行任何清理工作,例如释放会话使用的资源。您可以通过向 Global.asax 文件添加一个命名为 Session_OnEnd 的公共子例程,来为 End 事件指定一个处理程序。仅当会话状态 HttpSessionState.Mode 的属性值为 InProc 时 (默认值) Session_OnEnd 事件才会受到支持。如果会话状态 Mode 设置为 StateServer 或 SQLServer,Global.asax 文件中的 Session_OnEnd 事件将被忽略。如果会话状态 Mode 的属性值为 Custom,则自定义会话状态存储提供程序将确定支持 Session_OnEnd 事件。
  4. Global.asax 语法 - Global.asax 文件 (也称为 ASP.NET 应用程序文件) 是一个可选的文件,该文件包含响应 ASP.NET 或 HTTP 模块所引发的应用程序级别和会话级别事件的代码。Global.asax 文件驻留在 ASP.NET 应用程序的根目录中。运行时,分析 Global.asax 并将其编译到一个动态生成的 .NET Framework 类,该类是从 HttpApplication 基类派生的。配置 ASP.NET,以便自动拒绝对 Global.asax 文件的任何直接的 URL 请求; 外部用户不能下载或查看其中的代码。Global.asax 文件是可选的。只在希望处理应用程序事件或会话事件时,才应创建它。

Resource - 浏览器 多个标签 多个窗口 之间共享 SESSION

  1. IE SESSION 共享问题
  2. 同浏览器 不同窗口 共享 session - 一般的 IE 内核浏览器都是进程内共享 session-cookie。即同一个窗口内多个选项页共享 session,不同窗口独立 session。不曾想,IE8 居然创建性的实现所有 IE 进程共享 session。
  3. 多标签浏览器中 session 共享引发的问题
  4. 多个浏览器窗口中间不同的 Session - Firefox: 编辑快捷方式的目标,后面加上 -p -no-remote。Chrome: 打开新的隐身窗口。IE: 文件 --> 新的会话。

Resource

  1. 《AspNet4高级程序设计_第4版_MatthewMacDonald_AdamFreeman_MarioSzpuszta_人民邮电社_201106》 P194 6.5 会话状态
  2. Fish Li - Session, 有没有必要使用它?
  3. 使用 ETag 进行 session 的降级
  4. ASP.NET 中 session 过期设置方法
  5. cookie Session 机制的本质 & 跨应用程序的 session 共享
  6. ASP.NET - 在非 Page 类中使用 Session
  7. 全面了解 Session - 译文
  8. 全面了解 Session - 原文
  9. 一次性搞定 Session

Source Code of System.Web.SessionState.HttpSessionState from JetBrains dotPeek 1.0 at 2015-05-16

TonyRemark: 本类中 所有属性与方法 都是对 "字段 _container" 的简单包装 (调用同名属性或方法),仅 "Contents 属性 与 RemoveAll 方法" 例外。

Complete Copy:

// Type: System.Web.SessionState.HttpSessionState
// Assembly: System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Web.dll

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Web;

namespace System.Web.SessionState
{
  public sealed class HttpSessionState : ICollection, IEnumerable
  {
    private IHttpSessionState _container;

    internal IHttpSessionState Container
    {
      get
      {
        return this._container;
      }
    }

    public string SessionID
    {
      get
      {
        return this._container.SessionID;
      }
    }

    public int Timeout
    {
      get
      {
        return this._container.Timeout;
      }
      set
      {
        this._container.Timeout = value;
      }
    }

    public bool IsNewSession
    {
      get
      {
        return this._container.IsNewSession;
      }
    }

    public SessionStateMode Mode
    {
      get
      {
        return this._container.Mode;
      }
    }

    public bool IsCookieless
    {
      get
      {
        return this._container.IsCookieless;
      }
    }

    public HttpCookieMode CookieMode
    {
      get
      {
        return this._container.CookieMode;
      }
    }

    public int LCID
    {
      get
      {
        return this._container.LCID;
      }
      set
      {
        this._container.LCID = value;
      }
    }

    public int CodePage
    {
      get
      {
        return this._container.CodePage;
      }
      set
      {
        this._container.CodePage = value;
      }
    }

    public HttpSessionState Contents
    {
      get
      {
        return this;
      }
    }

    public HttpStaticObjectsCollection StaticObjects
    {
      get
      {
        return this._container.StaticObjects;
      }
    }

    public object this[string name]
    {
      get
      {
        return this._container[name];
      }
      set
      {
        this._container[name] = value;
      }
    }

    public object this[int index]
    {
      get
      {
        return this._container[index];
      }
      set
      {
        this._container[index] = value;
      }
    }

    public int Count
    {
      get
      {
        return this._container.Count;
      }
    }

    public NameObjectCollectionBase.KeysCollection Keys
    {
      get
      {
        return this._container.Keys;
      }
    }

    public object SyncRoot
    {
      get
      {
        return this._container.SyncRoot;
      }
    }

    public bool IsReadOnly
    {
      get
      {
        return this._container.IsReadOnly;
      }
    }

    public bool IsSynchronized
    {
      get
      {
        return this._container.IsSynchronized;
      }
    }

    internal HttpSessionState(IHttpSessionState container)
    {
      this._container = container;
    }

    public void Abandon()
    {
      this._container.Abandon();
    }

    public void Add(string name, object value)
    {
      this._container[name] = value;
    }

    public void Remove(string name)
    {
      this._container.Remove(name);
    }

    public void RemoveAt(int index)
    {
      this._container.RemoveAt(index);
    }

    public void Clear()
    {
      this._container.Clear();
    }

    public void RemoveAll()
    {
      this.Clear();
    }

    public IEnumerator GetEnumerator()
    {
      return this._container.GetEnumerator();
    }

    public void CopyTo(Array array, int index)
    {
      this._container.CopyTo(array, index);
    }
  }
}

会话劫持 (Session Hijacking)

会话劫持 指 攻击者 通过某种手段 获取 合法用户的 SessionID 后 以该合法用户的身份 使用网站的功能。

攻击者 获取 SessionID 的方式通常有:

  1. 暴力破解: 尝试各种 SessionID,直到破解为止;
  2. 预测: 如果 SessionID 使用非随机的方式产生,那么就有可能计算出来;
  3. 窃取: 使用 网络嗅探、XSS攻击 等方法获得。

防范措施:

  • 使用 复杂、难以预测 的规则生成 SessionID,增加前两种攻击方式的难度。现在各网站开发框架的 SessionID 算法均如此。
  • 网络嗅探 通过捕获网络通信数据 得到 SessionID,这种攻击可以通过 SSL 避免。
  • 将 cookie 设置为 HttpOnly 使得 XSS 攻击难度增加。

ASP.NET 常用操作

//写
Session["UserName"] = "***";

//读
string userName = Session["UserName"].ToString();

//遍历
IEnumerator sessionEnum = Session.Keys.GetEnumerator();
while (sessionEnum.MoveNext()) {
    Response.Write(Session[sessionEnum.Current.ToString()]);
}

//销毁
Session.Abandon(); //结束会话
Session.Clear(); //不结束会话

ASP.NET 中在不同的子域中共享 Session

以下代码 在每次会话开始时 把名为 ASP.NET_SessionId 的 Cookie 的 Value 重写为已有的 SessionID,并且把 Domain 重写为父域 .local.com,从而实现所有的二级域名 例如 a.local.com、b.local.com、user.local.com 跨域 Session 共享。

// This method will run when a new session is started.
protected void Session_Start(object sender, EventArgs e)
{
    Response.Cookies["ASP.NET_SessionId"].Value = Session.SessionID.ToString();
    Response.Cookies["ASP.NET_SessionId"].Domain = ".local.com";
}

Abandon(), Clear(), RemoveAll()

  1. RemoveAll() 的源码中仅仅是简单的调用了 Clear()。网摘: "二者之所以并存,只是因为 API 的设计惯例为提供 Remove(string name) + RemoveAt(int index) + RemoveAll() 全套"。
  2. 可以把 Session 理解为 网站应用程序域 中的一个全局字典 public Dictionary<string, HttpSessionState> _dic;,该字典的键就是 "ASP.NET_SessionId",该字典的值 HttpSessionState session = _dic["ASP.NET_SessionId"]; 即 特定客户端对应的 Session。Abandon()_dic 中移除了 特定用户的 "ASP.NET_SessionId" 表示的键,使得 sessionnullClear() 未移除 特定用户的 "ASP.NET_SessionId" 表示的键,只是清空了 session 中的项,使得 session.Count0
  3. InProc 模式下 Abandon() 会触发 Session_End 事件,导致下一个请求将触发 Session_StartClear() 不会触发这两个事件。
  1. Abandon 和 Clear 的实现和区别
  1. Session.Abandon 和 Session.Clear 的实现和区别

Session Lock

public static T.Account.CurrentStaff CurrentStaff
{
    get
    {
        if (HttpContext.Current.Session["CurrentStaff"] == null) {
            object obForLock = HttpContext.Current.Session["ObForLock"];
            if (obForLock == null) {
                obForLock = new object();
                HttpContext.Current.Session["ObForLock"] = obForLock;
            }
            bool isLoginSuccess = false;
            lock (obForLock) {
                //上一行判断导致仅当session过期后才会执行下一行来使用cookie登录。
                isLoginSuccess = T.Account.LoginOfStaff.LoginByCk();
            }
            if (!isLoginSuccess) {
                T.Account.CurrentStaff currentStaff = new T.Account.CurrentStaff();
                //下一行将上一行的空实例赋给session,就意味着除非session过期,否则不会再次用cookie登录。
                T.SessionPool.CurrentStaff = currentStaff;
                return currentStaff;
            }
        }
        return (T.Account.CurrentStaff)HttpContext.Current.Session["CurrentStaff"];
    }
    set
    {
        HttpContext.Current.Session["CurrentStaff"] = value;
    }
}