你有没有想过自己来为一个网站做一个手机客户端呢?
想要设计一个客户端,一般来说都需要实现模拟登陆功能,这样才能获取用户的个人信息,不然都直接通过手机浏览器网页来访问的话,效果不好且界面不友好
这里来模拟登陆我学校的图书馆,平台为安卓系统
一、准备工具
需要用到的工具库有两个:
1. Jsoup
jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据
2.android-async-http
android-async-http是一个强大的网络请求库,这个网络请求库是基于Apache HttpClient库之上的一个异步网络请求处理库,网络处理均基于Android的非UI线程,通过回调方法处理请求结果
下载然后将之导入工程中
二、了解思路
我学校的图书馆网址是:http://lib.wyu.edu.cn/Html/index.Html
个人账号登录网址是:http://lib.wyu.edu.cn/opac/login.aspx
正常来说我应该是在本界面登录的,输入学号、密码、验证码等信息,不过鼓捣了一下网站,发现了网站有个隐藏的登录页面:http://lib.wyu.edu.cn/opac/test.aspx
根据网址名可以判断出该页面应该是在开发网站时用来测试的
而这个页面居然不需要验证码=_=!
这样就省事很多了
现在就开始来研究下如何实现模拟登录,看看需要向服务器发送什么信息
用谷歌浏览器打开登录页,按F12键,点击Application标签,查看Cookie
图片箭头所指向的值即为当前用户的Cookie值,每次打开该页面,该值应该都是不同的,即用来唯一标示每位用户
转到Network标签,查看访问信息
箭头所指即为请求头,后边需要用到
可以看到请求头中的一项为Cookie
点击右键查看网页源代码,删去一些无用的代码,重点在于中间的表单form
<form name="form1" method="post" action="test.aspx" onsubmit="javascript:return WebForm_OnSubmit();" id="form1" target="_blank">
<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="/wEPDwUKLTY2Njg2ODg0Mw9kFgICAw8WAh4GdGFyZ2V0BQZfYmxhbmsWBAIDD2QWBGYPD2QWAh4MYXV0b2NvbXBsZXRlBQNvZmZkAgQPDxYCHgRUZXh0ZWRkAgUPZBYGZg8QZGQWAWZkAgEPEGRkFgFmZAICDw9kFgIfAQUDb2ZmZGTcY8B98vBh8r3/5k/FWW0LQrvmCw==" />
</div>
<div id="content">
<input name="txtlogintype" type="hidden" id="txtlogintype" value="0" />
<div class="LoginDiv">
<div class="loginContent">
<div class="in" style="margin-top:8px">
<span class="leftInfo">图书证号:</span>
<span class="rightInfo">
<input name="txtUsername_Lib" type="text" id="txtUsername_Lib" class="txtInput" autocomplete="off" style="width:100px;" /><span id="rfv_UserName_Lib" style="color:Red;display:none;">请输入证号</span> </span>
</div>
<div class="in" style="margin-top:8px">
<span class="leftInfo">密 码:</span>
<span class="rightInfo">
<input name="txtPas_Lib" type="password" id="txtPas_Lib" class="txtInput" style="width:100px;" /><span id="rfv_Password_Lib" style="color:Red;display:none;">请输入密码</span> </span>
</div>
<div>
<span id="lblErr_Lib"></span>
</div>
<div style="margin-top:15px">
<input type="submit" name="btnLogin_Lib" value="登录" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("btnLogin_Lib", "", true, "", "", false, false))" id="btnLogin_Lib" class="multiQuery" />
<input type="button" value="清空" onclick="rset()" class="multiQuery" />
</div>
</div>
</div>
</div>
<div>
<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWBQK2h7H9DgLxkDwLfkqekBgLN05+fBgKe9OnfBhxoxkpnmMwQ62JlcaByWkgdRCZp" />
</div>
</form>
form标签中的action="test.aspx"
意思是提交的数据需要Post给谁,这里是直接Post到本页面即可
而每一个input标签都是需要提交的数据,name值代表数据名,value即数据值
例如当登录时,提交的所有信息中就有名为“txtUsername_Lib”,值为学号的数据
模拟登录的过程简单来说,即用户首先访问登录页,获得了Cookie值,然后输入数据将每一项input数据Post给服务器,如果登录成功,则之后访问个人信息页面只需要带上Cookie值即可,因为此时服务器已经知道该Cookie对应哪位用户了
三、敲代码
现在就来正式敲代码实现模拟登陆了
为了简化网络请求操作,我简单封装了Get和Post操作
/**
* Get和Post操作的简单封装
* Created by ZY on 2016/10/29.
*/
public class NetAPI {
/**
* Get操作
*
* @param client client
* @param url url
* @param charset 编码格式
* @param callback 回调函数
*/
public static void HttpGet(AsyncHttpClient client, String url, String charset, final NetCallback callback) {
client.get(url, new TextHttpResponseHandler(charset) {
@Override
public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
if (callback != null) {
callback.onFailure("状态码:" + statusCode);
}
}
@Override
public void onSuccess(int statusCode, Header[] headers, String responseString) {
if (callback == null) {
return;
}
if (statusCode == HttpStatus.SC_OK) {
callback.onSuccess(headers, responseString);
} else {
callback.onFailure("");
}
}
});
}
/**
* Post操作
*
* @param client client
* @param url url
* @param params 请求参数
* @param charset 编码格式
* @param callback 回调函数
*/
public static void HttpPost(AsyncHttpClient client, String url, RequestParams params, String charset, final NetCallback callback) {
client.post(url, params, new TextHttpResponseHandler(charset) {
@Override
public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
if (callback != null) {
callback.onFailure("状态码:" + statusCode);
}
}
@Override
public void onSuccess(int statusCode, Header[] headers, String responseString) {
if (callback == null) {
return;
}
if (statusCode == HttpStatus.SC_OK) {
callback.onSuccess(headers, responseString);
} else {
callback.onFailure("");
}
}
});
}
}
用到的回调函数
/**
* 回调函数
* Created by ZY on 2016/10/29.
*/
public interface NetCallback {
void onFailure(String response);
void onSuccess(Header[] headers, String response);
}
新建一个LibraryAPI类,采用单例模式
//图书馆登录
private final static String LIBRARY_LOGIN_URL = "http://lib.wyu.edu.cn/opac/test.aspx";
//图书馆个人信息
private final static String USER_INFO_URL = "http://lib.wyu.edu.cn/opac/user/userinfo.aspx";
//当前借书
private final static String BOOK_BORROWED_URL = "http://lib.wyu.edu.cn/opac/user/bookborrowed.aspx";
//借书历史
private final static String BOOK_BORROWED_HISTORY_URL = "http://lib.wyu.edu.cn/opac/user/bookborrowedhistory.aspx?page=";
private AsyncHttpClient client;
private static LibraryAPI libraryAPI;
//私有化构造函数
private LibraryAPI(Context context) {
client = new AsyncHttpClient();
PersistentCookieStore cookieStore = new PersistentCookieStore(context);
cookieStore.clear();
client.setCookieStore(cookieStore);
//设置请求头
client.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
client.addHeader("Accept-Encoding", "gzip, deflate, sdch");
client.addHeader("Accept-Language", "zh-CN,zh;q=0.8");
client.addHeader("Cache-Control", "max-age=0");
client.addHeader("Connection", "Keep-Alive");
client.addHeader("Host", "lib.wyu.edu.cn");
client.addHeader("Referer", "http://lib.wyu.edu.cn/opac/search.aspx");
client.addHeader("Upgrade-Insecure-Requests", "1");
client.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36");
}
/**
* 获取实例
*
* @param context 上下文
* @return 实例
*/
public static LibraryAPI getInstance(Context context) {
if (libraryAPI == null) {
libraryAPI = new LibraryAPI(context);
}
return libraryAPI;
}
当中的请求头根据谷歌浏览器中查看的数据来设置即可,有些请求头不是必须的,不过为了省事就全带上了
需要注意的是以下一步:
PersistentCookieStore cookieStore = new PersistentCookieStore(context);
cookieStore.clear();
client.setCookieStore(cookieStore);
即将Cooki保存到本地,即Cookie持久化,每次重新调用时清理本地Cookie
之后就是来登录图书馆了
/***
* 登录图书馆
*
* @param studentID 学号
* @param password 密码
* @param callback 回调函数
*/
public void login(String studentID, String password, final NetCallback callback) {
//添加请求参数
final RequestParams params = new RequestParams();
params.add("__EVENTTARGET", "");
params.add("__EVENTARGUMENT", "");
params.add("__VIEWSTATE", "/wEPDwUKLTY2Njg2ODg0Mw9kFgICAw8WAh4GdGFyZ2V0BQZfYmxhbmsWBAIDD2QWBGYPD2QWAh4MYXV0b2NvbXBsZXRlBQNvZmZkAgQPDxYCHgRUZXh0ZWRkAgUPZBYGZg8QZGQWAWZkAgEPEGRkFgFmZAICDw9kFgIfAQUDb2ZmZGTcY8B98vBh8r3/5k/FWW0LQrvmCw==");
params.add("txtlogintype", "0");
params.add("btnLogin_Lib", "登录");
params.add("__EVENTVALIDATION", "/wEWBQK2h7H9DgLxkMjADwLfkqekBgLN05+fBgKe9OnfBhxoxkpnmMwQ62JlcaByWkgdRCZp");
params.add("txtUsername_Lib", studentID);
params.add("txtPas_Lib", password);
NetAPI.HttpGet(client, LIBRARY_LOGIN_URL, "UTF-8", new NetCallback() {
@Override
public void onFailure(String response) {
callback.onFailure(response + " 获取图书馆登录页失败");
}
@Override
public void onSuccess(Header[] headers, String response) {
NetAPI.HttpPost(client, LIBRARY_LOGIN_URL, params, "UTF-8", new NetCallback() {
@Override
public void onFailure(String response) {
callback.onFailure(response + " 登录图书馆失败");
}
@Override
public void onSuccess(Header[] headers, String response) {
for (Header header : headers) {
//Post后返回的headers中必须含有该header,才证明Post成功
if (header.getName().equals("Set-Cookie")) {
callback.onSuccess(headers, response);
return;
}
}
callback.onFailure("登录图书馆失败");
}
});
}
});
}
首先需要以Get方式访问登录页,此时Cookie值会被自动保存下来,Get成功后就可以来向登录页Post学号、密码等数据了,这些数据都保存在请求参数RequestParams当中
有些请求参数的名与值都是不变了,我也不知道Post到服务器到底有什么用处~~
此时,即使Post成功了,也不代表就登录成功了,用谷歌浏览器来查看,可以发现当登录成功后,返回的Header[]当中会带上一个名为“Set-Cookie”的数据
所以检查返回的Header[]即可知道是否已经登录成功
如果登录成功,就可以调用以下方法获取当前借阅书籍列表了
/**
* 获取当前借阅情况
*
* @param callback 回调函数
*/
public void getBookBorrowed(NetCallback callback) {
NetAPI.HttpGet(client, BOOK_BORROWED_URL, "UTF-8", callback);
}
为了方便,新建一个书籍实体Book
/**
* 在查询当前借书情况与借书历史时使用
* Created by ZY on 2016/10/29.
*/
public class Book {
/**
* 书名
*/
private String bookName;
/**
* 登录号
*/
private String id;
/**
* 借书日期
*/
private String borrowDate;
/**
* 书籍最迟应还日期/书籍还期
*/
private String deadline;
public Book(String bookName, String id, String borrowDate, String deadline) {
this.bookName = bookName;
this.id = id;
this.borrowDate = borrowDate;
this.deadline = deadline;
}
public String getBookName() {
return bookName;
}
public String getId() {
return id;
}
public String getBorrowDate() {
return borrowDate;
}
public String getDeadline() {
return deadline;
}
@Override
public String toString() {
return "Book{" +
"bookName='" + bookName + '\'' +
", id='" + id + '\'' +
", borrowDate='" + borrowDate + '\'' +
", deadline='" + deadline + '\'' +
'}' + "\n";
}
}
现在即可来获取书籍借阅列表了,将获取到的数据显示在TextView上
private void getBookList() {
final LibraryAPI libraryAPI = LibraryAPI.getInstance(this);
libraryAPI.login("填入学号", "填入密码", new NetCallback() {
@Override
public void onFailure(String response) {
tv_content.setText("登录失败");
}
@Override
public void onSuccess(Header[] headers, String response) {
libraryAPI.getBookBorrowed(new NetCallback() {
@Override
public void onFailure(String response) {
tv_content.setText("获取当前书籍借阅情况失败");
}
@Override
public void onSuccess(Header[] headers, String response) {
List<Book> bookList = HtmlParseHelper.parseBookBorrowed(response);
if (bookList == null) {
tv_content.setText("解析当前书籍借阅情况失败");
return;
}
if (bookList.size() == 0) {
tv_content.setText("当前木有借书");
return;
}
StringBuilder builder = new StringBuilder();
for (Book book : bookList) {
builder.append(book.toString());
}
tv_content.setText(builder.toString());
System.out.println(builder.toString());
}
});
}
});
}
当中需要用到一个工具类HtmlParseHelper,因为服务器返回的是Html代码,需要将之解析为格式友好的数据
/**
* 解析当前书籍借阅情况
*
* @param html html文件
* @return 书籍列表
*/
public static List<Book> parseBookBorrowed(String html) {
List<Book> bookList;
try {
Document document = Jsoup.parse(html);
Element divElement = document.select("div#borrowedcontent").first();
Element tbodyElement = divElement.getElementsByTag("tbody").first();
Elements trElements = tbodyElement.getElementsByTag("tr");
bookList = new ArrayList<>();
Elements tdElements;
Book book;
String bookName;
String id;
String borrowDate;
String deadline;
for (Element trElem : trElements) {
tdElements = trElem.getElementsByTag("td");
bookName = tdElements.get(2).text();
id = tdElements.get(5).text();
borrowDate = tdElements.get(6).text();
deadline = tdElements.get(1).text();
book = new Book(bookName, id, borrowDate, deadline);
bookList.add(book);
}
} catch (Exception e) {
return null;
}
return bookList;
}
获取到的数据如下:
数据没错,的确是我当前借的书
按照这方法,就可以获取到所有的个人信息,再设计好看点的UI界面,就可以完成一个图书馆客户端了~
四、补充
可能有人会说没有验证码的登录页面毕竟是少数,有验证码的页面又该如何操作?
其实即使有验证码,登录操作也就是麻烦了点,也并不难
这里再以我学校的学生服务子系统为例子,网址:http://jwc.wyu.edu.cn/student/body.htm
按F12查看Cookie,可以看到当中有一项名为“LogonNumber”的数据,值即为图片验证码当中的数字
这样只要先遍历Cookie值,取出验证码值,在Post时将之加入请求参数即可,也不需要用户来输入验证码值了,简化了操作
如果这样不行的话,也可以查看源代码获取验证码链接,在登录时下载该图片并显示,由用户输入验证码即可
方法有很多,只要多钻研下,总归是能够实现的
代码我已上传到GitHub上:模拟登陆网站实现移动客户端