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

时间:2022-09-17 23:48:54

一说到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。