一、原理
- 在超级课程表等APP中,大都有使用学号密码登录教务系统读取课程的功能。在这里我将模仿此功能使用J2V8、Jsoup等第三方库实现模拟登录教务系统读取信息。
- 这里的教务系统以南昌大学教务系统(湖南强智科技教务系统)为例。
现在的大多数网站架构都是后台使用Session来进行用户验证,并向前端返回Cookie,前端的每次请求都带上这个Cookie以作为用户登陆态的保持与身份的认证。
另有部分网站(尤其是APP)使用JWT(JSON Web Tokens)进行登陆态验证,确认登录后前端请求带上返回的Token,也是大同小异。这里主要以前者为例。
我们先来梳理一下登录的流程
1. 前端向后台Servlet发送请求,返回验证码图片。请求头中携带Cookie。
2.登录时,请求头中携带相应Cookie,并将账号密码编码后连同验证码一同post到后台
3.若后台验证账号密码与验证码正确,则该Cookie已经作为本用户登录态的凭证,每次请求携带该Cookie即可访问相应权限的Url。
二、具体实现
- 在Android代码中,我将所有Activity均继承于BaseActivity,BaseActivity中实现沉浸式标题栏的相应代码,将配置文件中的主题修改为@style/Theme.AppCompat.Light.NoActionBar以完成沉浸式标题栏。
/**
* 封装可复用的代码,作为所有Activity的父类
*/
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//region 沉浸式状态栏代码
// 5.0以上系统状态栏透明
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
//endregion
setContentView(getViewId());
}
/**
*添加抽象方法,供子类绑定视图
* @return
*/
abstract int getViewId();
}
- 继续进行功能分析,首先,我们要向相应的Servlet发送请求,将验证码显示在App上。
- 在这里我们使用的是jsoup: Java HTML Parser进行网络链接以及Cookie获取。
我们可以直接在jsoup官网下载对应jar包。为方便起见,我们在Maven Repository中找到jsoup的gradle依赖,并将其添加到build.gradle的dependencies中。
compile group: 'org.jsoup', name: 'jsoup', version: '1.11.2'
然后我们使用jsoup进行验证码的请求,并将Cookie信息保存下来。
(注:jsoup对网络的请求写在子线程中,通过handle对前端进行操作)
private String url_safecode = "http://jwc101.ncu.edu.cn/jsxsd/verifycode.servlet"; // 验证码
private Map<String, String> cookie;
class PicThread extends Thread{
@Override
public void run() {
Connection.Response response = null;
try {
response = Jsoup.connect(url_safecode)
.ignoreContentType(true) // 获取图片需设置忽略内容类型
.userAgent("Mozilla")
.method(Connection.Method.GET).timeout(3000).execute();
cookie = response.cookies(); //将cookie保存下来
byte[] bytes = response.bodyAsBytes();
//验证码保存为Bitmap
Bitmap bitmap= BitmapFactory.decodeByteArray(bytes,0,bytes.length);
Message message=new Message();
message.what=0x123;
message.obj=bitmap;
handler.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Handle接受到Message后将验证码显示在界面的ImageView上。
handler=new Handler(){
@Override
public void handleMessage(Message msg) {
if(msg.what==0x123){
imageView.setImageBitmap((Bitmap)msg.obj);
}
}
};
OK,我们现在将验证码显示了出来并保存了Cookie,接下来就是将账号与密码Post到后台。这里要注意一个问题,我们通过之前的截图可以看到,登录时Post的账号与密码都是重新编码过的。
我们通过右键查看网页源代码可以看到,该编码过程账号和密码分别是调用conwork.js中的encodeInp方法,然后将编码后的密文用%%%进行连接。
- 这里有两种解决方案,一种解决方案是将加密算法使用Java重写一遍,这种方法比较麻烦,我在这里采用的是第二种方法,直接在Android中使用J2V8框架运行JS代码进行加密。
J2V8框架的添加方式与jsoup大同小异,在maven repository中找到J2V8的gradle依赖,然后添加到build.gradle中。
compile 'com.eclipsesource.j2v8:j2v8:4.5.0@aar'
将conwork.js放到项目目录的assets中,然后使用J2V8调用。
try {
InputStream is=getAssets().open("conwork.js"); //获取用户名与密码加密的js代码
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line = null;
try {
while ((line = reader.readLine()) != null) {
sb.append(line);
}
V8 runtime = V8.createV8Runtime(); //使用J2V8运行js代码并将编码结果返回
final String encodename = runtime.executeStringScript(sb.toString()
+ "encodeInp('"+userin.getText().toString()+"');\n");
final String encodepwd=runtime.executeStringScript(sb.toString()+"encodeInp('"+pwdin.getText().toString()+"');\n");
runtime.release();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
然后我们将encodename与encodepwd使用jsoup来post到教务系统登录的url,post时带上之前保存的cookie。
(new Thread(){
@Override
public void run() {
Map<String, String> data = new HashMap<String, String>(); //进行Post的参数
data.put("encoded", encodename+"%%%"+encodepwd);
data.put("RANDOMCODE", verin.getText().toString());
Connection connect = Jsoup.connect(url_Login)
.header("Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
.userAgent("Mozilla").method(Connection.Method.POST).data(data).timeout(3000);
for (Map.Entry<String, String> entry : cookie.entrySet()) { //使用获取验证码时生成的cookie
connect.cookie(entry.getKey(), entry.getValue());
}
Connection.Response response = null;
try {
response = connect.execute();
Pattern p=Pattern.compile("<title>(.*)</title>");
Matcher m =p .matcher(response.body()) ;
if( m. find()) {
//通过response的title是否为"学生个人中心"来进行判断是否登录成功
if((m.group(1).toString()).equals("学生个人中心")){
//登录成功的相应代码
}else{
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
这时我们就已经完成了模拟登录,之后可以使用这个cookie访问管理系统的各个url了。