利用HttpWebRequest和HttpWebResponse獲取Cookie並實現模擬登陸

     以前看過某個同窗的一篇有關與使用JSoup解析學校圖書館的文章,仔細一看,發現居然是同校!!既然對方用的是java,那麼我也就來個C#好了,雖然個人入門語言是java。javascript

     C#沒有JSoup這樣方便的東西,我也沒有仔細去找,由於只要利用正則表達式,一樣能夠很好的解析網頁內容而不須要其餘幫助。如今作前端的程序員,若是正則表達式不熟悉,反而去依賴第三方的話,感受很惋惜!html

     這是咱們學校圖書館的登陸界面的body:前端

<body onload="bodyload()">
<form name="aspnetForm" method="post" action="login.aspx?ReturnUrl=%2fuser%2fuserinfo.aspx" onsubmit="javascript:return WebForm_OnSubmit();" id="aspnetForm">
<div>
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwULLTE0MjY3MDAxNzcPZBYCZg9kFgoCAQ8PFgIeCEltYWdlVXJsBRt+XGltYWdlc1xoZWFkZXJvcGFjNGdpZi5naWZkZAICDw8WAh4EVGV4dAUt5bm/5Lic5bel5Lia5aSn5a2m5Zu+5Lmm6aaG5Lmm55uu5qOA57Si57O757ufZGQCAw8PFgIfAQUcMjAxM+W5tDA35pyIMjHml6UgIOaYn+acn+aXpWRkAgQPZBYEZg9kFgQCAQ8WAh4LXyFJdGVtQ291bnQCCBYSAgEPZBYCZg8VAwtzZWFyY2guYXNweAAM55uu5b2V5qOA57SiZAICD2QWAmYPFQMTcGVyaV9uYXZfY2xhc3MuYXNweAAM5YiG57G75a+86IiqZAIDD2QWAmYPFQMOYm9va19yYW5rLmFzcHgADOivu+S5puaMh+W8lWQCBA9kFgJmDxUDCXhzdGIuYXNweAAM5paw5Lmm6YCa5oqlZAIFD2QWAmYPFQMUcmVhZGVycmVjb21tZW5kLmFzcHgADOivu+iAheiNkOi0rWQCBg9kFgJmDxUDE292ZXJkdWVib29rc19mLmFzcHgADOaPkOmGkuacjeWKoWQCBw9kFgJmDxUDEnVzZXIvdXNlcmluZm8uYXNweAAP5oiR55qE5Zu+5Lmm6aaGZAIID2QWAmYPFQMbaHR0cDovL2xpYnJhcnkuZ2R1dC5lZHUuY24vAA/lm77kuabppobpppbpobVkAgkPZBYCAgEPFgIeB1Zpc2libGVoZAIDDxYCHwJmZAIBD2QWBAIDD2QWBAIBDw9kFgIeDGF1dG9jb21wbGV0ZQUDb2ZmZAIHDw8WAh8BZWRkAgUPZBYGAgEPEGRkFgFmZAIDDxBkZBYBZmQCBQ8PZBYCHwQFA29mZmQCBQ8PFgIfAQWlAUNvcHlyaWdodCAmY29weTsyMDA4LTIwMDkuIFNVTENNSVMgT1BBQyA0LjAxIG9mIFNoZW56aGVuIFVuaXZlcnNpdHkgTGlicmFyeS4gIEFsbCByaWdodHMgcmVzZXJ2ZWQuPGJyIC8+54mI5p2D5omA5pyJ77ya5rex5Zyz5aSn5a2m5Zu+5Lmm6aaGIEUtbWFpbDpzenVsaWJAc3p1LmVkdS5jbmRkZLjlteIKM9K+qxtjyYb5tuBVJpjN" />
</div>

<script type="text/javascript">
//<![CDATA[
var theForm = document.forms['aspnetForm'];
if (!theForm) {
    theForm = document.aspnetForm;
}
function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}
//]]>
</script>


<script src="/WebResource.axd?d=aUGQsxtqBlPCYSLCotjFAgPO7LVRMgZpwdLaRZWG0_6ihSU9GEuAV24Gz4casq4yN9Ey0mGcWUzl2dmajXQUJps-v9o1&amp;t=635090739058261250" type="text/javascript"></script>


<script src="/WebResource.axd?d=ID2SHi1EXLOLcv8QZV5z65ofzpfIKQP67HbOJyDtBOZGRBT6-d--Al86w9CE4E-H3dCnvuE2ZcqgPXnod-92Tv-ZeIo1&amp;t=635090739058261250" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
function WebForm_OnSubmit() {
if (typeof(ValidatorOnSubmit) == "function" && ValidatorOnSubmit() == false) return false;
return true;
}
//]]>
</script>

<div>

    <input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWBQLuwfDDAwKOmK5RApX9wcYGAsP9wL8JAqW86pcI915B4NnPkyRLP7m6znmysZd+180=" />
</div>
<input name="ctl00$ContentPlaceHolder1$txtlogintype" type="hidden" id="ctl00_ContentPlaceHolder1_txtlogintype" value="0" />
<div id="Login" class="clearFix">
    <div class="LoginTitle">
        登陸個人圖書館
    </div>
    
    
    <div class="LeftLogin">
            <div class="LoginDiv">
                
                <div class="loginContent">
                    <div class="loginInfo">
                        <span class="leftInfo">圖書證號:</span>
                        <span class="rightInfo">
                            <input name="ctl00$ContentPlaceHolder1$txtUsername_Lib" type="text" id="ctl00_ContentPlaceHolder1_txtUsername_Lib" class="txtInput" autocomplete="off" /><span id="ctl00_ContentPlaceHolder1_rfv_UserName_Lib" style="color:Red;display:none;">請輸入證號</span>
                        </span>
                    </div>
                    <div class="loginInfo">
                        <span class="leftInfo">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;碼:</span>
                        <span class="rightInfo">
                            <input name="ctl00$ContentPlaceHolder1$txtPas_Lib" type="password" id="ctl00_ContentPlaceHolder1_txtPas_Lib" class="txtInput" /><span id="ctl00_ContentPlaceHolder1_rfv_Password_Lib" style="color:Red;display:none;">請輸入密碼</span>
                        </span>
                    </div>
                    <div>
                        <span id="ctl00_ContentPlaceHolder1_lblErr_Lib"></span>
                    </div>
                    <div class="loginInfo">
                        <input type="submit" name="ctl00$ContentPlaceHolder1$btnLogin_Lib" value="登陸" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;ctl00$ContentPlaceHolder1$btnLogin_Lib&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, false))" id="ctl00_ContentPlaceHolder1_btnLogin_Lib" class="btn" />
                        <input type="button" value="清空" onclick="rset()" class="btn"/>
                    </div>
                </div>
            </div>
        </div>
        <div class="RightDescription">
        <img src="images/pin.gif" />  <br/>
        1.  若是您使用的是公共電腦,請在使用完畢後,務必退出登陸,以保安全。<br />
        2.  首次登陸,請先<a href="changepas.aspx">修改初始密碼</a></div>
        
        
        
    
    
</div>

<script type="text/javascript">
//<![CDATA[
var Page_Validators =  new Array(document.getElementById("ctl00_ContentPlaceHolder1_rfv_UserName_Lib"), document.getElementById("ctl00_ContentPlaceHolder1_rfv_Password_Lib"));
//]]>
</script>

<script type="text/javascript">
//<![CDATA[
var ctl00_ContentPlaceHolder1_rfv_UserName_Lib = document.all ? document.all["ctl00_ContentPlaceHolder1_rfv_UserName_Lib"] : document.getElementById("ctl00_ContentPlaceHolder1_rfv_UserName_Lib");
ctl00_ContentPlaceHolder1_rfv_UserName_Lib.controltovalidate = "ctl00_ContentPlaceHolder1_txtUsername_Lib";
ctl00_ContentPlaceHolder1_rfv_UserName_Lib.focusOnError = "t";
ctl00_ContentPlaceHolder1_rfv_UserName_Lib.errormessage = "請輸入證號";
ctl00_ContentPlaceHolder1_rfv_UserName_Lib.display = "Dynamic";
ctl00_ContentPlaceHolder1_rfv_UserName_Lib.evaluationfunction = "RequiredFieldValidatorEvaluateIsValid";
ctl00_ContentPlaceHolder1_rfv_UserName_Lib.initialvalue = "";
var ctl00_ContentPlaceHolder1_rfv_Password_Lib = document.all ? document.all["ctl00_ContentPlaceHolder1_rfv_Password_Lib"] : document.getElementById("ctl00_ContentPlaceHolder1_rfv_Password_Lib");
ctl00_ContentPlaceHolder1_rfv_Password_Lib.controltovalidate = "ctl00_ContentPlaceHolder1_txtPas_Lib";
ctl00_ContentPlaceHolder1_rfv_Password_Lib.focusOnError = "t";
ctl00_ContentPlaceHolder1_rfv_Password_Lib.errormessage = "請輸入密碼";
ctl00_ContentPlaceHolder1_rfv_Password_Lib.display = "Dynamic";
ctl00_ContentPlaceHolder1_rfv_Password_Lib.evaluationfunction = "RequiredFieldValidatorEvaluateIsValid";
ctl00_ContentPlaceHolder1_rfv_Password_Lib.initialvalue = "";
//]]>
</script>


<script type="text/javascript">
//<![CDATA[

var Page_ValidationActive = false;
if (typeof(ValidatorOnLoad) == "function") {
    ValidatorOnLoad();
}

function ValidatorOnSubmit() {
    if (Page_ValidationActive) {
        return ValidatorCommonOnSubmit();
    }
    else {
        return true;
    }
}
        //]]>
</script>
</form>
</body>

      最主要的是<form></form>裏面的內容,也就是咱們要提交的表單。要實現登陸,就要將表單提交上去,固然,在這以前,咱們先要填充該表單。java

      首先,咱們須要提取出須要填充的內容。直接看源代碼,咱們已經知道須要填充什麼數據了,但注意到咱們要提交的表單須要填充__VIEWSTATE和__EVENTVALIDATION,而它們的表單值每次都不同,因此咱們須要提取出來,而不是本身填充。程序員

      ViewState是ASP.NET用來保存控件狀態信息的。在ASP時代,一個HTML控件的值,好比input控件值會在咱們把表單提交到服務器,頁面再刷新回來的時候自動清空掉,這是由於web的無狀態致使的,因此服務端每次把HTML輸出到客戶端後就再也不與客戶端有任何聯繫。web

      可是ASP.NET解決了這個問題:當咱們在寫一個ASP.NET表單的時候,若是標明"form runat = server",那麼ASP.NET就會自動在輸出時給頁面添加一個隱藏域:正則表達式

<input type = "hidden" name = "_VIEWSTATE" value = "">

      有了這個隱藏域,頁面裏其餘全部控件的狀態,包括頁面自己的一些狀態都會保存到這個控件值裏,這樣每次頁面提交時就會一塊兒提交到後臺,而後由ASP.NET對其中的值進行解碼,最後輸出時再根據這個值來恢復各個控件的狀態。
      在上面獲得的網頁源碼中,我麼能夠看到,_VIEWSTATE的值很是複雜,根本沒法閱讀,就像是加密後的值同樣,但實際上並無任何加密,僅僅是由於各個控件和頁面的狀態都存入適當的對象裏面,而後把該對象序列化並進行一次Base64編碼,最後直接賦值給viewstate控件。所謂的Base64就是一種基於64個可打印字符來表示二進制數據的表示方法,一般用於處理像是MIME的Email或者XML中存儲的一些複雜數據,在HTTP環境中專門用來傳遞較長的標識信息,用做HTTP表單和HTTP GET URL中的參數。瀏覽器

      固然,咱們會想到:爲何不用Session來保存這些數據呢?由於Session值是保存在服務器內存上的,若是大量使用Session將致使服務器負擔加劇,而ViewState只是將數據存入到頁面隱藏控件裏,不會佔用服務器資源。因此,咱們能夠將一些須要服務器保存的變量和對象保存到ViewState裏,而Session只應該用在須要跨頁面而且與每一個訪問用戶相關的變量和對象存儲上。最重要的一點,Session默認狀況下20分鐘就過時了,但ViewState永遠不會過時。安全

      ViewState並不能存儲全部的.NET數據類型,它僅僅支持String , Integer, Boolean, Array, ArrayList, HashTable,以及一些自定義的類型。服務器

       雖然使用ViewState對服務器內存是友好的,但使用ViewState會增長頁面HTML的輸出量,像是上面那麼一大串的字符串,這樣帶寬就會被佔用得更多。並且ViewState是不安全的,由於全部的ViewState都存儲在同一個地方,咱們徹底能夠經過查看源碼來找到這個值,只要通過轉換就能夠獲取其中的對象和變量值。

       爲了解決這個問題,ASP.NET提供了兩種方案:防篡改和加密。這些措施是題外話了,你們有興趣的能夠本身研究下。

       __EVENTVALIDATION是ASP.NET2.0新增長的特性(很遺憾,咱們學校圖書館使用的就是ASP.NET 2.0,已經斷代了好久了,但這種公共管理網站是不會更新的,除非哪天它出現了嚴重問題),它用於對PostBack的值進行驗證,確保其是合法的值。他的工做原理是這樣的:在頁面render的時候,ASP.NET會對控件全部可能的值以及控件的UniqueID進行Hash計算,獲得一個值,全部須要Render的控件都會有一個這樣的計算值,而且組成一個列表,而後放在隱藏字段__EVENTVALIDATION中,頁面Render後,就會對該字段的額內容進行解包,從新計算並對比Hash值是否一致。這樣是爲了防範一些模擬的post攻擊,但問題也就來了:若是頁面很是大或者網速加載較慢,用戶在沒有加載到該字段的時候就提交,會發數據不完整,那麼就會出錯。

      經過上面的內容,咱們已經明白:要使表單提交成功,咱們就要獲得這兩個值。

        private List<string> GetElementContent(string url, string elementName, string subElement)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            Stream responseStream = response.GetResponseStream();
            StreamReader reader = new StreamReader(responseStream, Encoding.UTF8);
            string content = reader.ReadToEnd();
            reader.Close();
            responseStream.Close();

            StringBuilder regexStr = new StringBuilder("(?is)<");
            regexStr.Append(elementName).Append("[^>]*?").Append(subElement).Append(@"=(['""\s]?)([^'""\s]+)\1[^>]*?>");
            Regex regex = new Regex(regexStr.ToString());
            MatchCollection match = regex.Matches(content);
            List<string> values = new List<string>();
            foreach (Match m in match)
            {
                values.Add(m.Groups[2].Value);
            }
            return values;
        }

      該方法就是利用正則表達式對HTML頁面中的節點內容進行提取。首先,咱們得先獲得該頁面,因而須要經過HttpWebRequest發出請求,而後由HttpWebResponse獲得響應該請求後返回的頁面,接着就是利用正則表達式提取了。

      若是有心想搞Web的同窗,正則表達式必定要懂得寫。若是對正則表達式的使用駕輕就熟的話,在實際編碼中就會很是方便。

      咱們來看看這個方法是如何使用的:

 List<string> values = GetElementContent(url, "input", "value");
 List<string> names = GetElementContent(url, "input", "name");

     這樣咱們就獲得了表單中全部要提交的元素的name和value了。

     接着就是自動填充表單了:

Dictionary<string, string> postPair = new Dictionary<string, string>();
for (int i = 0, len = names.Count(); i < len; i++)
{
     postPair.Add(names.ElementAtOrDefault(i), values.ElementAtOrDefault(i));
}

     利用一個字典,將對應的name和value放進去。固然,要想登陸,咱們仍是得須要用戶名和密碼:

SetParams("ctl00$ContentPlaceHolder1$txtUsername_Lib", "***", postPair);
SetParams("ctl00$ContentPlaceHolder1$txtPas_Lib", "***", postPair);

     SetParams()方法的實現以下:

private void SetParams(string element, string content, Dictionary<string, string> dictionary)
        {
            for (int i = 0, len = dictionary.Keys.Count(); i < len; i++)
            {
                string key = dictionary.Keys.ElementAtOrDefault(i);
                if (key == element)
                {
                    dictionary[key] = content;
                }
            }
        }

      這樣的值咱們是不能提交上去的,還須要進行URL編碼:

        private void UrlEncodeParams(Dictionary<string, string> formParams)
        {
            for (int i = 0, len = formParams.Keys.Count(); i < len; i++)
            {
                string key = formParams.Keys.ElementAtOrDefault(i);
                formParams[key] = HttpUtility.UrlEncode(formParams[key], Encoding.GetEncoding("GBK"));
            }
        }

     而後就是Post該表單了:

  private string PostForm(string url, Dictionary<string, string> form)
        {
            CookieContainer cookies = new CookieContainer();
            string postStr = "";
            foreach (string key in form.Keys)
            {
                postStr += key + "=" + form[key] + "&";
            }
            byte[] postData = Encoding.ASCII.GetBytes(postStr.Substring(0, postStr.Length - 1));
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "POST";
            request.AllowAutoRedirect = false;
            request.ContentType = "application/x-www-form-urlencoded;charset=gbk";
            request.CookieContainer = new CookieContainer();
            request.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11";
            request.ContentLength = postData.Length;

            Stream requestStream = request.GetRequestStream();
            requestStream.Write(postData, 0, postData.Length);
            requestStream.Close();

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            Stream responseStream = response.GetResponseStream();
            StreamReader reader = new StreamReader(responseStream);

            string cookie = response.Headers.Get("Set-Cookie");
            string resultPage = reader.ReadToEnd();
            string html = getHtml(GetCookieName(cookie), GetCookieValue(cookie));
            reader.Close();
            responseStream.Close();
            return html;
        }

       該方法纔是咱們的重點!
       咱們要先定義一個CookieContainer用來存儲獲得的Cookie。提交的表單字符串的形式應該是這樣:name1=value1&name2=value2&...。一樣,提交上去的時候,咱們也是須要對該字符串進行編碼。

       重點在於接下來的HttpWebRequest和HttpWebResponse的使用。

       首先,咱們的HttpRequest應該是post方式,能夠經過request.Method進行設置。request.AllowAutoRedirect=false是爲了防止重定向,不少網站都是登陸後就立刻進行重定向,這樣咱們就沒法獲得登陸時的Cookie,因而須要禁止該重定向。而後就是request.ContentType = "application/x-www-form-urlencoded;charset=gbk"。Content-Type就是所謂的內容類型,它是HTTP請求的頭部信息,用於指定消息的類型,默認是text/plain,即純文本,這裏是application/x-www-form-urlencode,即指定是使用HTTP的POST方式提交表單。而後咱們再指定請求的CookieContainer爲剛纔定義的CookieContainer。而後就是指定代理:request.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"。用戶代理字符串用於標識請求的瀏覽器類型和版本,操做系統,使用語言等各類信息,主要就是用於抓包。採用POST方式提交,都須要指定內容長度:request.ContentLength = postData.Length。

       接着咱們再向請求流中寫入數據:

Stream requestStream = request.GetRequestStream();
requestStream.Write(postData, 0, postData.Length);
requestStream.Close();

       獲得響應後,咱們就能夠獲得響應中的Cookie:

string cookie = response.Headers.Get("Set-Cookie");

       一般在響應的頭信息裏就包含了Cookie,它就是Set-Cookie的值。若是想要知道HTTP的頭信息有哪些,能夠在谷歌瀏覽器中按F12進行查看,但記得,要想捕捉這些信息,必須在未登陸前就按F12,不然谷歌瀏覽器是不會跟蹤這些信息的。
       而後咱們就能夠利用該Cookie登陸網站而且獲得登陸後的網站內容:

        string html = getHtml(GetCookieName(cookie), GetCookieValue(cookie));

        private string GetCookieValue(string cookie)
        {
            Regex regex = new Regex("=.*?;");
            Match value = regex.Match(cookie);
            string cookieValue = value.Groups[0].Value;
            return cookieValue.Substring(1, cookieValue.Length - 2);
        }

        private string GetCookieName(string cookie)
        {
            Regex regex = new Regex("sulcmiswebpac.*?");
            Match value = regex.Match(cookie);
            return value.Groups[0].Value;
        }

        private string getHtml(string name, string value)
        {
            CookieCollection cookies = new CookieCollection();
            cookies.Add(new Cookie(name, value));
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://222.200.98.171:81/user/bookborrowed.aspx");
            request.Method = "GET";
            request.Headers.Add("Cookie", name + "=" + value);

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            Stream stream = response.GetResponseStream();
            StreamReader reader = new StreamReader(stream, Encoding.UTF8);
            return reader.ReadToEnd();
        }

     咱們以前獲得的cookie字符串中其實就是一個鍵值對字符串,咱們仍是須要根據正則表達式提取出cookie的name和value,而後咱們定義一個CookieContainer,往裏面添加咱們捕捉到的Cookie,接着就是須要Cookie的網頁發送一個帶有該Cookie的HTTP請求。
     方法很簡單,只要合理的使用正則表達式,咱們就能夠方便的對網頁進行處理,而不須要什麼第三方的庫。

     每一個程序員都須要學會本身造輪子而不是一味的追求輪子,就算是其餘語言的輪子,咱們依然能夠用本身熟悉的語言實現出來,畢竟全部的語言背後的實現思想都是同樣的,尤爲是面嚮對象語言,它們都是相互借鑑的,交叉處實在是太多了,C#更是在參考java的基礎上創造出來的,有什麼理由是java能夠C#不能夠呢?(有是有,但咱們能夠模擬)

相關文章
相關標籤/搜索