Asp.net cookie的处理流程深入分析

  一说到Cookie我想大家都应该知道它是一个保存在客户端,当浏览器请求一个url时,浏览器会携带相关的Cookie达到服务器端,所以服务器是可以操作Cookie的,在Response时,会把Cookie信息输出到客服端。下面我们来看一个demo吧,代码如下:

Asp.net cookie的处理流程深入分析

  第一次请求结果如下:

Asp.net cookie的处理流程深入分析

  第二次请求结果如下:

Asp.net cookie的处理流程深入分析

  到这里我们可以看到第二次请求传入的Cookie正好是第一次请求返回的Cookie信息,这里的cookie信息的维护主要是我们客户端的浏览器,但是在Asp.net程序开发时,Cookie往往是在服务端程序里面写入,就如我的事例代码;很少有用客服端js实现的。现在我们就来看看asp.net服务端是如何实现读写Cookie的。

  首先我们来看看HttpRequest的Cookie是如何定义的:

  

复制代码 代码如下:

  public HttpCookieCollection Cookies {

  get {

  EnsureCookies();

  if (_flags[needToValidateCookies]) {

  _flags.Clear(needToValidateCookies);

  ValidateCookieCollection(_cookies);

  }

  return _cookies;

  }

  }

  这里的Cookie获取主要是调用一个EnsureCookies方法,EnsureCookies放主要是调用

  

复制代码 代码如下:

  // Populates the Cookies property but does not hook up validation.

  internal HttpCookieCollection EnsureCookies() {

  if (_cookies == null) {

  _cookies = new HttpCookieCollection(null, false);

  if (_wr != null)

  FillInCookiesCollection(_cookies, true /*includeResponse*/);

  if (HasTransitionedToWebSocketRequest) // cookies can't be modified after the WebSocket handshake is complete

  _cookies.MakeReadOnly();

  }

  return _cookies;

  }

  public sealed class HttpCookieCollection : NameObjectCollectionBase

  {

  internal HttpCookieCollection(HttpResponse response, bool readOnly) : base(StringComparer.OrdinalIgnoreCase)

  {

  this._response = response;

  base.IsReadOnly = readOnly;

  }

  }

  其中这里的FillInCookiesCollection方法实现也比较复杂:

  

复制代码 代码如下:

  internal void FillInCookiesCollection(HttpCookieCollection cookieCollection, bool includeResponse) {

  if (_wr == null)

  return;

  String s = _wr.GetKnownRequestHeader(HttpWorkerRequest.HeaderCookie);

  // Parse the cookie server variable.

  // Format: c1=k1=v1&k2=v2; c2=...

  int l = (s != null) ? s.Length : 0;

  int i = 0;

  int j;

  char ch;

  HttpCookie lastCookie = null;

  while (i < l) {

  // find next ';' (don't look to ',' as per 91884)

  j = i;

  while (j < l) {

  ch = s[j];

  if (ch == ';')

  break;

  j++;

  }

  // create cookie form string

  String cookieString = s.Substring(i, j-i).Trim();

  i = j+1; // next cookie start

  if (cookieString.Length == 0)

  continue;

  HttpCookie cookie = CreateCookieFromString(cookieString);

  // some cookies starting with '$' are really attributes of the last cookie

  if (lastCookie != null) {

  String name = cookie.Name;

  // add known attribute to the last cookie (if any)

  if (name != null && name.Length > 0 && name[0] == '$') {

  if (StringUtil.EqualsIgnoreCase(name, "$Path"))

  lastCookie.Path = cookie.Value;

  else if (StringUtil.EqualsIgnoreCase(name, "$Domain"))

  lastCookie.Domain = cookie.Value;

  continue;

  }

  }

  // regular cookie

  cookieCollection.AddCookie(cookie, true);

  lastCookie = cookie;

  // goto next cookie

  }

  // Append response cookies

  if (includeResponse) {

  // If we have a reference to the response cookies collection, use it directly

  // rather than going through the Response object (which might not be available, e.g.

  // if we have already transitioned to a WebSockets request).

  HttpCookieCollection storedResponseCookies = _storedResponseCookies;

  if (storedResponseCookies == null && !HasTransitionedToWebSocketRequest && Response != null) {

  storedResponseCookies = Response.GetCookiesNoCreate();

  }

  if (storedResponseCookies != null && storedResponseCookies.Count > 0) {

  HttpCookie[] responseCookieArray = new HttpCookie[storedResponseCookies.Count];

  storedResponseCookies.CopyTo(responseCookieArray, 0);

  for (int iCookie = 0; iCookie < responseCookieArray.Length; iCookie++)

  cookieCollection.AddCookie(responseCookieArray[iCookie], append: true);

  }

  // release any stored reference to the response cookie collection

  _storedResponseCookies = null;

  }

  }

  说简单一点它主要调用HttpWorkerRequest的GetKnownRequestHeader方法获取浏览器传进来的Cookie字符串信息,然后再把这些信息根据;来分隔成多个HttpCookie实例。把这些HttpCookie实例添加到传进来的HttpCookieCollection参数。

  这里HttpWorkerRequest继承结果如下:

  

复制代码 代码如下:

  internal class ISAPIWorkerRequestInProcForIIS7 : ISAPIWorkerRequestInProcForIIS6

  internal class ISAPIWorkerRequestInProcForIIS6 : ISAPIWorkerRequestInProc

  internal class ISAPIWorkerRequestInProc : ISAPIWorkerRequest

  internal abstract class ISAPIWorkerRequest : HttpWorkerRequest

  其中 GetKnownRequestHeader方法的实现主要是在ISAPIWorkerRequest中,其GetKnownRequestHeader主要是调用了它的ReadRequestHeaders私有方法,在ReadRequestHeaders方法中主要是调用它的this.GetServerVariable("ALL_RAW")方法,所以我们可以认为this.GetServerVariable("ALL_RAW")这个方法是获取客户端传来的Cookie参数,而GetServerVariable方法的实现主要是在ISAPIWorkerRequestInProc 类,具体实现非常复杂。

  

  这里的GetKnownRequestHeader方法实现非常复杂我们也就不去深研它了,我们只要知道调用这个方法就会返回Cookie的所有字符串信息。在这个方法里面还调用了一个CreateCookieFromString方法,根据字符串来创建我们的HttpCookie实例。CreateCookieFromString方法实现如下:

  

复制代码 代码如下:

  internal static HttpCookie CreateCookieFromString(String s) {

  HttpCookie c = new HttpCookie();

  int l = (s != null) ? s.Length : 0;

  int i = 0;

  int ai, ei;

  bool firstValue = true;

  int numValues = 1;

  // Format: cookiename[=key1=val2&key2=val2&...]

  while (i < l) {

  // find next &

  ai = s.IndexOf('&', i);

  if (ai < 0)

  ai = l;

  // first value might contain cookie name before =

  if (firstValue) {

  ei = s.IndexOf('=', i);

  if (ei >= 0 && ei < ai) {

  c.Name = s.Substring(i, ei-i);

  i = ei+1;

  }

  else if (ai == l) {

  // the whole cookie is just a name

  c.Name = s;

  break;

  }

  firstValue = false;

  }

  // find '='

  ei = s.IndexOf('=', i);

  if (ei < 0 && ai == l && numValues == 0) {

  // simple cookie with simple value

  c.Value = s.Substring(i, l-i);

  }

  else if (ei >= 0 && ei < ai) {

  // key=value

  c.Values.Add(s.Substring(i, ei-i), s.Substring(ei+1, ai-ei-1));

  numValues++;

  }

  else {

  // value without key

  c.Values.Add(null, s.Substring(i, ai-i));

  numValues++;

  }

  i = ai+1;

  }

  return c;

  }

  我们平时很少用到HttpCookie的Values属性,所以这个属性大家还是需要注意一下,这个方法就是把一个cookie的字符串转化为相应的HttpCookie实例。

  现在我们回到HttpRequest的Cookies属性中来,这里有一个关于Cookie的简单验证

  

复制代码 代码如下:

  private void ValidateCookieCollection(HttpCookieCollection cc) {

  if (_enableGranularValidation) {

  // Granular request validation is enabled - validate collection entries only as they're accessed.

  cc.EnableGranularValidation((key, value) => ValidateString(value, key, RequestValidationSource.Cookies));

  }

  else {

  // Granular request validation is disabled - eagerly validate all collection entries.

  int c = cc.Count;

  for (int i = 0; i < c; i++) {

  String key = cc.GetKey(i);

  String val = cc.Get(i).Value;

  if (!String.IsNullOrEmpty(val))

  ValidateString(val, key, RequestValidationSource.Cookies);

  }

  }

  }

  其中HttpCookieCollection的EnableGranularValidation实现如下:

  

复制代码 代码如下:

  internal void EnableGranularValidation(ValidateStringCallback validationCallback)

  {

  this._keysAwaitingValidation = new HashSet<string>(this.Keys.Cast<string>(), StringComparer.OrdinalIgnoreCase);

  this._validationCallback = validationCallback;

  }

  private void EnsureKeyValidated(string key, string value)

  {

  if ((this._keysAwaitingValidation != null) && this._keysAwaitingValidation.Contains(key))

  {

  if (!string.IsNullOrEmpty(value))

  {

  this._validationCallback(key, value);

  }

  this._keysAwaitingValidation.Remove(key);

  }

  }

  到这里我们知道默认从浏览器发送到服务器端的Cookie都是需要经过次验证的。这里的ValidateString方法具体实现我们就不说了,不过大家需要知道它是调用了RequestValidator.Current.IsValidRequestString方法来实现验证的,有关RequestValidator的信息大家可以查看HttpRequest的QueryString属性 的一点认识 。现在我们获取Cookie已经基本完成了。那么我们接下来看看是如何添加Cookie的了。

  首先我们来看看HttpResponse的Cookie属性:

  

复制代码 代码如下:

  public HttpCookieCollection Cookies

  {

  get

  {

  if (this._cookies == null)

  {

  this._cookies = new HttpCookieCollection(this, false);

  }

  return this._cookies;

  }

  }

  接下来我们看看HttpCookie的实现如下:

  

复制代码 代码如下:

  public sealed class HttpCookie {

  private String _name;

  private String _path = "/";

  private bool _secure;

  private bool _httpOnly;

  private String _domain;

  private bool _expirationSet;

  private DateTime _expires;

  private String _stringValue;

  private HttpValueCollection _multiValue;

  private bool _changed;

  private bool _added;

  internal HttpCookie() {

  _changed = true;

  }

  /*

  * Constructor - empty cookie with name

  */

  /// <devdoc>

  /// <para>

  /// Initializes a new instance of the <see cref='System.Web.HttpCookie'/>

  /// class.

  /// </para>

  /// </devdoc>

  public HttpCookie(String name) {

  _name = name;

  SetDefaultsFromConfig();

  _changed = true;

  }

  /*

  * Constructor - cookie with name and value

  */

  /// <devdoc>

  /// <para>

  /// Initializes a new instance of the <see cref='System.Web.HttpCookie'/>

  /// class.

  /// </para>

  /// </devdoc>

  public HttpCookie(String name, String value) {

  _name = name;

  _stringValue = value;

  SetDefaultsFromConfig();

  _changed = true;

  }

  private void SetDefaultsFromConfig() {

  HttpCookiesSection config = RuntimeConfig.GetConfig().HttpCookies;

  _secure = config.RequireSSL;

  _httpOnly = config.HttpOnlyCookies;

  if (config.Domain != null && config.Domain.Length > 0)

  _domain = config.Domain;

  }

  /*

  * Whether the cookie contents have changed

  */

  internal bool Changed {

  get { return _changed; }

  set { _changed = value; }

  }

  /*

  * Whether the cookie has been added

  */

  internal bool Added {

  get { return _added; }

  set { _added = value; }

  }

  // DevID 251951 Cookie is getting duplicated by ASP.NET when they are added via a native module

  // This flag is used to remember that this cookie came from an IIS Set-Header flag,

  // so we don't duplicate it and send it back to IIS

  internal bool FromHeader {

  get;

  set;

  }

  /*

  * Cookie name

  */

  /// <devdoc>

  /// <para>

  /// Gets

  /// or sets the name of cookie.

  /// </para>

  /// </devdoc>

  public String Name {

  get { return _name;}

  set {

  _name = value;

  _changed = true;

  }

  }

  /*

  * Cookie path

  */

  /// <devdoc>

  /// <para>

  /// Gets or sets the URL prefix to transmit with the

  /// current cookie.

  /// </para>

  /// </devdoc>

  public String Path {

  get { return _path;}

  set {

  _path = value;

  _changed = true;

  }

  }

  /*

  * 'Secure' flag

  */

  /// <devdoc>

  /// <para>

  /// Indicates whether the cookie should be transmitted only over HTTPS.

  /// </para>

  /// </devdoc>

  public bool Secure {

  get { return _secure;}

  set {

  _secure = value;

  _changed = true;

  }

  }

  /// <summary>

  /// Determines whether this cookie is allowed to participate in output caching.

  /// </summary>

  /// <remarks>

  /// If a given HttpResponse contains one or more outbound cookies with Shareable = false (the default value),

  /// output caching will be suppressed for that response. This prevents cookies that contain potentially

  /// sensitive information, e.g. FormsAuth cookies, from being cached in the response and sent to multiple

  /// clients. If a developer wants to allow a response containing cookies to be cached, he should configure

  /// caching as normal for the response, e.g. via the OutputCache directive, MVC's [OutputCache] attribute,

  /// etc., and he should make sure that all outbound cookies are marked Shareable = true.

  /// </remarks>

  public bool Shareable {

  get;

  set; // don't need to set _changed flag since Set-Cookie header isn't affected by value of Shareable

  }

  /// <devdoc>

  /// <para>

  /// Indicates whether the cookie should have HttpOnly attribute

  /// </para>

  /// </devdoc>

  public bool HttpOnly {

  get { return _httpOnly;}

  set {

  _httpOnly = value;

  _changed = true;

  }

  }

  /*

  * Cookie domain

  */

  /// <devdoc>

  /// <para>

  /// Restricts domain cookie is to be used with.

  /// </para>

  /// </devdoc>

  public String Domain {

  get { return _domain;}

  set {

  _domain = value;

  _changed = true;

  }

  }

  /*

  * Cookie expiration

  */

  /// <devdoc>

  /// <para>

  /// Expiration time for cookie (in minutes).

  /// </para>

  /// </devdoc>

  public DateTime Expires {

  get {

  return(_expirationSet ? _expires : DateTime.MinValue);

  }

  set {

  _expires = value;

  _expirationSet = true;

  _changed = true;

  }

  }

  /*

  * Cookie value as string

  */

  /// <devdoc>

  /// <para>

  /// Gets

  /// or

  /// sets an individual cookie value.

  /// </para>

  /// </devdoc>

  public String Value {

  get {

  if (_multiValue != null)

  return _multiValue.ToString(false);

  else

  return _stringValue;

  }

  set {

  if (_multiValue != null) {

  // reset multivalue collection to contain

  // single keyless value

  _multiValue.Reset();

  _multiValue.Add(null, value);

  }

  else {

  // remember as string

  _stringValue = value;

  }

  _changed = true;

  }

  }

  /*

  * Checks is cookie has sub-keys

  */

  /// <devdoc>

  /// <para>Gets a

  /// value indicating whether the cookie has sub-keys.</para>

  /// </devdoc>

  public bool HasKeys {

  get { return Values.HasKeys();}

  }

  private bool SupportsHttpOnly(HttpContext context) {

  if (context != null && context.Request != null) {

  HttpBrowserCapabilities browser = context.Request.Browser;

  return (browser != null && (browser.Type != "IE5" || browser.Platform != "MacPPC"));

  }

  return false;

  }

  /*

  * Cookie values as multivalue collection

  */

  /// <devdoc>

  /// <para>Gets individual key:value pairs within a single cookie object.</para>

  /// </devdoc>

  public NameValueCollection Values {

  get {

  if (_multiValue == null) {

  // create collection on demand

  _multiValue = new HttpValueCollection();

  // convert existing string value into multivalue

  if (_stringValue != null) {

  if (_stringValue.IndexOf('&') >= 0 || _stringValue.IndexOf('=') >= 0)

  _multiValue.FillFromString(_stringValue);

  else

  _multiValue.Add(null, _stringValue);

  _stringValue = null;

  }

  }

  _changed = true;

  return _multiValue;

  }

  }

  /*

  * Default indexed property -- lookup the multivalue collection

  */

  /// <devdoc>

  /// <para>

  /// Shortcut for HttpCookie$Values[key]. Required for ASP compatibility.

  /// </para>

  /// </devdoc>

  public String this[String key]

  {

  get {

  return Values[key];

  }

  set {

  Values[key] = value;

  _changed = true;

  }

  }

  /*

  * Construct set-cookie header

  */

  internal HttpResponseHeader GetSetCookieHeader(HttpContext context) {

  StringBuilder s = new StringBuilder();

  // cookiename=

  if (!String.IsNullOrEmpty(_name)) {

  s.Append(_name);

  s.Append('=');

  }

  // key=value&...

  if (_multiValue != null)

  s.Append(_multiValue.ToString(false));

  else if (_stringValue != null)

  s.Append(_stringValue);

  // domain

  if (!String.IsNullOrEmpty(_domain)) {

  s.Append("; domain=");

  s.Append(_domain);

  }

  // expiration

  if (_expirationSet && _expires != DateTime.MinValue) {

  s.Append("; expires=");

  s.Append(HttpUtility.FormatHttpCookieDateTime(_expires));

  }

  // path

  if (!String.IsNullOrEmpty(_path)) {

  s.Append("; path=");

  s.Append(_path);

  }

  // secure

  if (_secure)

  s.Append("; secure");

  // httponly, Note: IE5 on the Mac doesn't support this

  if (_httpOnly && SupportsHttpOnly(context)) {

  s.Append("; HttpOnly");

  }

  // return as HttpResponseHeader

  return new HttpResponseHeader(HttpWorkerRequest.HeaderSetCookie, s.ToString());

  }

  }

  现在我们回到HttpCookieCollection的Add方法看看,

  

复制代码 代码如下:

  public void Add(HttpCookie cookie) {

  if (_response != null)

  _response.BeforeCookieCollectionChange();

  AddCookie(cookie, true);

  if (_response != null)

  _response.OnCookieAdd(cookie);

  }

  public sealed class HttpResponse

  {

  internal void BeforeCookieCollectionChange()

  {

  if (this._headersWritten)

  {

  throw new HttpException(SR.GetString("Cannot_modify_cookies_after_headers_sent"));

  }

  }

  internal void OnCookieAdd(HttpCookie cookie)

  {

  this.Request.AddResponseCookie(cookie);

  }

  }

  public sealed class HttpRequest

  {

  internal void AddResponseCookie(HttpCookie cookie)

  {

  if (this._cookies != null)

  {

  this._cookies.AddCookie(cookie, true);

  }

  if (this._params != null)

  {

  this._params.MakeReadWrite();

  this._params.Add(cookie.Name, cookie.Value);

  this._params.MakeReadOnly();

  }

  }

  }

  到这里我们应该知道每添加或修改一个Cookie都会调用HttpResponse的BeforeCookieCollectionChange和OnCookieAdd方法,BeforeCookieCollectionChange是确认我们的cookie是否可以添加的,以前在项目中就遇到这里的错误信息说什么“在header发送后不能修改cookie”,看见默认情况下_headersWritten是false,那么它通常在哪里被设置为true了,在HttpReaponse的BeginExecuteUrlForEntireResponse、Flush、EndFlush方法中被设置为true,而我们最常接触到的还是Flush方法。这里的OnCookieAdd方法确保Cookie实例同时也添加到HttpRequest中。

  

复制代码 代码如下:

  internal void AddCookie(HttpCookie cookie, bool append) {

  ThrowIfMaxHttpCollectionKeysExceeded();

  _all = null;

  _allKeys = null;

  if (append) {

  // DevID 251951 Cookie is getting duplicated by ASP.NET when they are added via a native module

  // Need to not double add response cookies from native modules

  if (!cookie.FromHeader) {

  // mark cookie as new

  cookie.Added = true;

  }

  BaseAdd(cookie.Name, cookie);

  }

  else {

  if (BaseGet(cookie.Name) != null) {

  // mark the cookie as changed because we are overriding the existing one

  cookie.Changed = true;

  }

  BaseSet(cookie.Name, cookie);

  }

  }

  private void ThrowIfMaxHttpCollectionKeysExceeded() {

  if (Count >= AppSettings.MaxHttpCollectionKeys) {

  throw new InvalidOperationException(SR.GetString(SR.CollectionCountExceeded_HttpValueCollection, AppSettings.MaxHttpCollectionKeys));

  }

  }

  这里的AddCookie方法也非常简单,不过每次添加都会去检查Cookie的个数是否超过最大值。其实添加Cookie还可以调用HttpResponse的AppendCookie方法,

  

复制代码 代码如下:

  public void AppendCookie(HttpCookie cookie)

  {

  if (this._headersWritten)

  {

  throw new HttpException(SR.GetString("Cannot_append_cookie_after_headers_sent"));

  }

  this.Cookies.AddCookie(cookie, true);

  this.OnCookieAdd(cookie);

  }

  这里它的实现和HttpCookieCollection的     public void Add(HttpCookie cookie)方法实现一致。

  同样我们也知道这些Cookie是在HttpResponse的GenerateResponseHeadersForCookies方法中被使用,

  其中GenerateResponseHeadersForCookies方法的实现如下:

  

复制代码 代码如下:

  internal void GenerateResponseHeadersForCookies()

  {

  if (_cookies == null || (_cookies.Count == 0 && !_cookies.Changed))

  return; // no cookies exist

  HttpHeaderCollection headers = Headers as HttpHeaderCollection;

  HttpResponseHeader cookieHeader = null;

  HttpCookie cookie = null;

  bool needToReset = false;

  // Go through all cookies, and check whether any have been added

  // or changed. If a cookie was added, we can simply generate a new

  // set cookie header for it. If the cookie collection has been

  // changed (cleared or cookies removed), or an existing cookie was

  // changed, we have to regenerate all Set-Cookie headers due to an IIS

  // limitation that prevents us from being able to delete specific

  // Set-Cookie headers for items that changed.

  if (!_cookies.Changed)

  {

  for(int c = 0; c < _cookies.Count; c++)

  {

  cookie = _cookies[c];

  if (cookie.Added) {

  // if a cookie was added, we generate a Set-Cookie header for it

  cookieHeader = cookie.GetSetCookieHeader(_context);

  headers.SetHeader(cookieHeader.Name, cookieHeader.Value, false);

  cookie.Added = false;

  cookie.Changed = false;

  }

  else if (cookie.Changed) {

  // if a cookie has changed, we need to clear all cookie

  // headers and re-write them all since we cant delete

  // specific existing cookies

  needToReset = true;

  break;

  }

  }

  }

  if (_cookies.Changed || needToReset)

  {

  // delete all set cookie headers

  headers.Remove("Set-Cookie");

  // write all the cookies again

  for(int c = 0; c < _cookies.Count; c++)

  {

  // generate a Set-Cookie header for each cookie

  cookie = _cookies[c];

  cookieHeader = cookie.GetSetCookieHeader(_context);

  headers.SetHeader(cookieHeader.Name, cookieHeader.Value, false);

  cookie.Added = false;

  cookie.Changed = false;

  }

  _cookies.Changed = false;

  }

  }

  这里我们还是来总结一下吧:在HttpWorkerRequest中我们调用 GetKnownRequestHeader方法来获取Cookie的字符串形式,然后再将这里的字符串转化为HttpCookie集合供 HttpRequest使用,在HttpResponse中的GenerateResponseHeadersForCookies方法中会处理我们的 cookie实例,调用cookie的GetSetCookieHeader方法得到HttpCookie对应的字符串值,然后把该值添加到 HttpHeaderCollection 集合中(或者修改已有的值)。在获取cookie是这里有一个验证需要我们注意的就是 RequestValidator.Current.IsValidRequestString方法。   在添加或修改Cookie是有2个地方的检查(1)检查Cookie的个数是否达到我们配置的cookie最大个数,(2)现在是否已经写入头信息,如果 头信息已经写了则不能操作cookie。