1. 程式人生 > >安卓課程表(解決利用Httpclient登入獲得cookie繼續訪問但網頁仍提示無許可權請登入的問題)

安卓課程表(解決利用Httpclient登入獲得cookie繼續訪問但網頁仍提示無許可權請登入的問題)

          第一次寫CSDN部落格,想一想還是有點小緊張。嘿嘿,希望能幫到和我一樣剛入門的菜鳥coder解決特定問題。如果有幸被各路大牛看到此文,還望不吝賜教,指正文章中可能存在的錯誤。 

       安卓課程表的最主要目的是將教務網中的課程表資訊(後面稱之為“資料”)獲取到,再顯示於安卓應用介面上。需要解決的問題無非只有兩個:1. 獲得“資料”;2. 將“資料”顯示在設計好的介面上。本文主要說如何獲得“資料”。

       在瀏覽器上看到自己課程表需要如下兩個過程:

       1. 登入(下圖是學校教務網登入介面):


       2.點選“本學期選課”,獲得課程表資訊


       安卓課程表就是模擬瀏覽器這一行為,從教務網伺服器上獲取到“資料”。如下圖:

       登入介面:

       

        獲取課程表資訊介面:


       最開始,我以為要實現類似“超級課程表”這樣的應用需要像新浪微博API那樣進行Oauth2認證,才能獲得微博上的指定資料的JSON或XML格式,自己再去解析。這樣豈不是需要向教務網索要介面等等。。。後來參考這篇博文:http://blog.csdn.net/u010858238/article/details/9029653(建議可以先直接去看這篇文章,如果看完後,具體實現中遇到問題,再回頭來看我寫的,可能我們遇到的問題一模一樣。後文也全是在假設已經看過這篇文章的基礎上寫的),發現利用Httpclient,這個過程無非就是在安卓系統上仿瀏覽器獲得資料的原理來實現,而且這樣對於任何有網頁端而無客戶端的東西都可以用類似的方法去實現某一網站的客戶端程式。


我參考的那篇文章已經非常清楚地教給大家原理,而我之所以再寫一篇文章的原因是:在自己實現安卓課程表的過程中,會遇到非常多意想不到的問題,而且整個過程並不是參考的那一篇文章所能解決的,原理性的東西只是起參考作用,實踐過程中需要自己去解決很多問題,比如:為什麼獲得了cookie但總是提示無許可權訪問,請先登入?為什麼一登入程式就停止執行?

       我在實現應用的過程中遵循下面的思路:

       步驟1. 利用Httpclient登入教務網,獲得可以繼續訪問網站後續功能的可以作為“憑證”的cookie;

       步驟2. 帶著這個cookie繼續訪問獲得課程表資訊(後面稱之為“資料”);

       步驟3. 會發現步驟2中獲得的“資料”其實是課程表頁面的HTML檔案(利用谷歌瀏覽器,在課程表網頁點選右鍵審查元素看到的程式碼),利用JSoup解析HTML檔案獲得需要的指定資訊,將之儲存於SQlite資料庫中,前端介面讀取資料並顯示在listview中(根據實際顯示介面而定)。

       在步驟1中,我最開始陷入了誤區,以為只要獲得cookie就可以繼續訪問,但實際情況是:獲得cookie並開始執行步驟2時總是提示無許可權訪問,請先登入。對這個問題,谷歌了幾乎所有與Httpclient有關的博文,英文幫助文件,看完了網上關於HTTP協議的各種講解後,發現坑不在這裡,而在學校教務網與自己的理解上。對於cookie,我最初以為只有登入成功後,伺服器才會給客戶端一個供後續訪問的憑證cookie,而實際情況是,即使登入失敗,伺服器也會給客戶端一個cookie,只是這個cookie是沒有許可權的,無法繼續訪問後續只有登入了才能訪問的介面。說白了,這個cookie對我們沒用。發現這個問題的過程是:最初我用程式獲得cookie,並帶著這個cookie訪問後續網頁,發現無許可權。後來,我先在谷歌瀏覽器上登入教務網,然後在登入成功的頁面上點選審查元素,找到左上角的Network並點選,按F5重新整理,重新載入頁面。點選需要的元素,此處我需要index.jsp這個頁面。如下圖:


        右邊Headers下Request Headers中的Cookie欄位的JSESSIONID正是我們所需要的。我將這個值直接傳入程式,發現成功獲取到課程表資訊的HTML檔案,也就是說,我用程式獲得的cookie只是一個無許可權的cookie,只要我獲得一個有許可權的cookie就可以成功解決問題。

        所以,當利用Httpclient登入獲得cookie繼續訪問但網頁仍提示無許可權請登入時,應該首先考慮是否獲得了一個有許可權的cookie?

        之後,我再次從瀏覽器上登入,這才發現了問題所在:原來學校教務網在登入介面(http://***.***.***.***/service/login.jsp)上提交表單資料後會先將資料交給servlet處理,再轉發到一個新頁面。而我正是忽略了這個介面,如下圖:

        困擾了這麼久,原來是urlLogin寫錯了,我錯誤地寫成了登入介面的URL(http://***.***.***.***/service/login.jsp),正確的應該是http://***.***.***.***/servlet/UserLoginSQLAction,因為我需要將表單資料提交給它。這一點覺得學校教務網在設計上有點奇怪,除錯的過程中我發現,即使登入失敗,返回的HttpStatusCode仍是200,最開始也誤導我,讓我一直以為我登入成功了,根本沒把問題歸結在我其實就沒登入成功上。

        在步驟3中,由於既有網路操作,又有資料庫操作,所以需要將這些耗時的操作放在子執行緒中,而安卓系統不允許子執行緒更新UI執行緒,所以,像彈出Toast這樣的提示框需要用到Handler(網上一搜一大堆教程)。我的做法是:將步驟1登入的過程放到一個執行緒當中,將獲取課程表資訊並存放“資料”到資料庫的過程放到一個執行緒當中,利用join()方法控制執行緒執行的順序,在成功執行完後,向Handler發訊息,讓Handler配合主執行緒更新UI,彈出Toast提示框。相關程式碼如下:

定義必要的全域性變數:

	private static HttpClient httpclient = new DefaultHttpClient();// 建立一個客戶端實體
	public static UserLoginInfo userLoginInfo = new UserLoginInfo();// 建立一個使用者實體
	private static String urlLogin = "http://***.***.***.***/servlet/UserLoginSQLAction";// 登入URL
	private static String urlCourse = "http://***.***.***.***/student/course/MyCourseThisTerm.jsp";// 課程資訊URL
	private String CourseHTML = null;// 儲存獲得的課程表網頁HTML檔案的String型別
        private static List<Cookie> cookies; // 儲存登陸成功後的cookie
        private static ArrayList<CourseInfo> allCourse;// 所有課程
登入子執行緒:
class PostThread extends Thread {
		String Tuser_id;
		String Tpassword;

		public PostThread(String user_id, String password) {
			this.Tuser_id = user_id;
			this.Tpassword = password;
		}

		public void run() {
			HttpPost httpPost = new HttpPost(urlLogin);// Post方法
			HttpResponse httpResponseForPost = null;// Post方法的響應資訊
			List<NameValuePair> params = new ArrayList<NameValuePair>();

			userLoginInfo.setUser_id(Tuser_id);// 學號
			userLoginInfo.setPassword(Tpassword);// 密碼
			userLoginInfo.setUser_style("modern");// 瀏覽器上的介面風格,可要可不要
			userLoginInfo.setUser_type("student");// 使用者型別

			/* 利用Post方法登入,目的是為了獲取能繼續訪問網頁的“通行證”cookie(登陸成功後的cookie) */
			/* Post方法引數設定 */
			params.add(new BasicNameValuePair("Browser", ""));
			params.add(new BasicNameValuePair("btn1", ""));
			params.add(new BasicNameValuePair("OperatingSystem", ""));
			params.add(new BasicNameValuePair("password", userLoginInfo
					.getPassword()));
			params.add(new BasicNameValuePair("url", "../usersys/index.jsp"));
			params.add(new BasicNameValuePair("user_id", userLoginInfo
					.getUser_id()));
			params.add(new BasicNameValuePair("user_style", userLoginInfo
					.getUser_style()));// 瀏覽器上的介面風格,可要可不要
			params.add(new BasicNameValuePair("user_type", userLoginInfo
					.getUser_type()));

			/*
			 * 設定必要的request header:Host頭域指定請求資源的Intenet主機和埠號,
			 * 必須表示請求url的原始伺服器或閘道器的位置。 HTTP/1.1請求必須包含主機頭域,否則系統會以400狀態碼返回。
			 */
			httpPost.setHeader("Host", "***.***.***.***");
			/*
			 * 設定必要的request header:Referer 頭域允許客戶端指定請求uri的源資源地址,
			 * 這可以允許伺服器生成回退連結串列,可用來登陸、優化cache等。 他也允許廢除的或錯誤的連線由於維護的目的被 追蹤。
			 * 如果請求的uri沒有自己的uri地址,Referer不能被髮送。 如果指定的是部分uri地址,則此地址應該是一個相對地址。
			 * 參考自http://blog.csdn.net/rainysia/article/details/8131174
			 */
			httpPost.setHeader("Referer",
					"http://***.***.***.***/service/login.jsp?user_type=student");
			try {
				httpPost.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
				httpResponseForPost = httpclient.execute(httpPost);
				if (httpResponseForPost.getStatusLine().getStatusCode() == 200) {
					cookies = ((AbstractHttpClient) httpclient)
							.getCookieStore().getCookies();

					// 利用SharedPreferences儲存使用者登入資訊
					SharedPreferences userSharedPreferences = getSharedPreferences(
							"user", Activity.MODE_PRIVATE);
					SharedPreferences.Editor editor = userSharedPreferences
							.edit();

					editor.putString("user_id", userLoginInfo.getUser_id());

					editor.commit();

					/*Message msg = new Message();
					msg.what = 1;
					handler.sendMessage(msg);*/
				} else {
					/*Message msg = new Message();
					msg.what = 2;
					handler.sendMessage(msg);*/
				}
			} catch (ClientProtocolException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} finally {

			}
		}
	}
獲取課程表資訊子執行緒:
class getThread extends Thread {
		String GTuser_id;

		public getThread(String user_id) {
			this.GTuser_id = user_id;
		}

		public void run() {
			HttpGet httpGet = new HttpGet(urlCourse);// Get方法
			HttpResponse httpResponseForGet = null;// Get方法的響應資訊
			/* 利用Get方法,帶上成功登入獲得的cookie,繼續訪問課程表頁面,並將頁面HTML格式全部放在String中 */
			httpGet.setHeader("Cookie",
					"JSESSIONID=" + cookies.get(0).getValue() + "; user_id="
							+ userLoginInfo.getUser_id() + "; user_type="
							+ userLoginInfo.getUser_type() + "; user_style="
							+ userLoginInfo.getUser_style() + ";");
			try {
				httpResponseForGet = httpclient.execute(httpGet);
				if (httpResponseForGet.getStatusLine().getStatusCode() == 200) {
					StringBuffer stringBuffer = new StringBuffer();
					HttpEntity entity = httpResponseForGet.getEntity();
					InputStream inputStream = null;
					inputStream = entity.getContent();
					BufferedReader br = null;
					br = new BufferedReader(new InputStreamReader(inputStream,
							"UTF-8"));
					String data = "";
					while ((data = br.readLine()) != null) {
						stringBuffer.append(data);
					}
					CourseHTML = stringBuffer.toString();
				}
			} catch (ClientProtocolException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} finally {
				// httpGet.releaseConnection();
			}
			// 利用Jsoup解析獲得的HTML檔案,可以忽略,因具體實現而異
			int flag = 1, idIndex = 4, nameIndex = 1, teacherIndex = 4, infoIndex = 1;
			if ("".equals(CourseHTML) && null == CourseHTML) {
				System.out.println("HTML null");
			}
			Document document = Jsoup.parse(CourseHTML);
			Elements courseId = document.select("td[width=8%]");
			Elements courseName = document.select("td[width=17%]");
			Elements courseTeacher = document.select("td[width=6%]");
			Elements courseInfo = document.select("td[width=20%]");
			Elements userName = document.select("td[height=32]");

			userLoginInfo.setUser_name(userName.text());
			ArrayList<String> id = getElement(courseId.text());
			ArrayList<String> name = getElement(courseName.text());
			ArrayList<String> teacher = getElement(courseTeacher.text());
			ArrayList<String> info = getElement(courseInfo.text());
int idSize = 0,nameSize = 0,teacherSize = 0,infoSize = 0;
idSize = id.size();
nameSize = name.size();
teacherSize = teacher.size();
infoSize = info.size();
			while (flag != 0) {
				CourseInfo course = new CourseInfo();
				course.setCourseId(id.get(idIndex).toString());
				course.setCourseName(name.get(nameIndex).toString());
				course.setCourseTeacher(teacher.get(teacherIndex).toString());
				course.setCourseInfo(info.get(infoIndex).toString()
						+ info.get(infoIndex + 1).toString());
				allCourse.add(course);
				idIndex += 4;
				nameIndex += 1;
				teacherIndex += 3;
				infoIndex += 2;
				if (idIndex > idSize || nameIndex > nameSize || teacherIndex > teacherSize
|| infoIndex > infoSize) {
flag = 0;
}
			}
			for (int i = 0; i < allCourse.size(); i++) {
				StringBuffer stringBuffer = new StringBuffer();
				stringBuffer.append(allCourse.get(i).getCourseTeacher() + "\n")
						.append(allCourse.get(i).getCourseInfo());
				courseDataBase.insertCourse(course_id, GTuser_id, allCourse
						.get(i).getCourseName(), stringBuffer.toString());
				Intent intent = new Intent();
				intent.setAction("action.refreshFriend");
				sendBroadcast(intent);
				showCountId();
			}
			/*Message msg = new Message();
			msg.what = 3;
			handler.sendMessage(msg);*/
		}
	}

        貼的程式碼中包含的很多自定義的方法沒有列出具體程式碼。