开发初衷
在校园中每次使用校园网都需要手动登录一次,有时甚至不会自动跳转登录页面,造成一些不便。所以打算编写一款个人使用(在这个前提下,则不必考虑诸多额外因素,比如系统兼容、帐号密码设置、人性化提示等)的小工具,实现一键登录。
实现方式
笔者的设备系统是 Android Nougat / 7.1.2,所以可充分利用 Nougat 的新特性,把这个小工具做成通知栏快捷设置图块(Quick Settings Tile)。这样,在连接到校园网 WiFi 后,下拉通知,点击图块即可实现登录。
成果截图:
关于这个新特性,在一些国产安卓系统上大概是不存在的,它们的通知栏被深度定制过,相关 API 被阉割。
知识点
- Chrome 开发者工具分析登录接口
- Chrome 接口调试工具 Postman 测试接口
- 新的开发语言之 Kotlin 而非 Java
- Android 网络开发基础(原生
HttpURLConnection
API)- Nougat 新特性之快捷设置图块(Quick Settings Tile)API
- 矢量图标(Vector Drawable)的选取
一、分析登录接口
笔者使用熟悉的 Chrome 浏览器及其开发者工具来分析。
提示:在登录成功后会跳转另外的站点,而这会导致开发者工具里记录的请求日志消失。解决方法是在开发者工具的工具栏里勾选 ☑️Preserve log 选项进行长连接记录日志。
从截获的请求数据中可以梳理出以下要点:
登录接口URL | http://172.20.255.252/ |
请求方式 | POST |
请求头部(Headers)数据 | 初步判断可忽略(结果证明确如此) |
表单数据 | DDDDD 为用户名upass 为密码(显然,经过加密,初步判断为 MD5 加密)其余字段 R1 、R2 、para 、0MKKey 初步判断为定值 |
接下来,查看网页源码搞清楚表单数据的生成规则(尤其是密码)。
定位到,找到在 POST 之前处理参数的函数
ee()
。
在ee()
函数中找到密码加密的关键代码。不难看出,加密方式为加盐值(salt)的 MD5 加密。pid
值(常量,值为1
) + 明文密码 + calg
值(常量,值为12345678
)经过 MD5 加密,然后加密值 + pid
值 + calg
值 就是最后的密文值了。
使用 Postman (一个 Chrome 应用,用于接口调试)进行测试。
测试成功。至此,笔者已经搞清楚登录接口的调用方法。
二、实现 Android APP
笔者用 Visio 画了功能流程图:
本例将选用 Kotlin 来进行整个开发。Kotlin 是一门开发 Android 的新兴语言,最近在 Google I/O 2017 大会上被推为 Android 开发官方语言。因此有必要熟悉并掌握它。去了解 Kotlin
首先,笔者跟随 Kotlin 官网提供的教程一步步在 Android Studio 中完成 Kotlin 项目的搭建。
本着偷懒的精神,笔者在以下两个站点选择了一个合适的图块图标:
- Google 家的 Material icons - Material Design
- Austin Andrews 个人维护的 Material Design Icons
笔者在上文提到这款 APP 为自用,所以兼容SDK版本直接上跳到24
(Nougat,7.0)
AndroidManifest 关键配置:
编写工具方法:
package com.by_syk.onetapcdutnet.util
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkInfo
object Net {
/**
* 判断 WiFi 连接
*/
fun isWiFiConnected(context: Context): Boolean {
val manager: ConnectivityManager? = context.getSystemService(Context.CONNECTIVITY_SERVICE)
as ConnectivityManager
val networkInfo: NetworkInfo? = manager?.activeNetworkInfo
return networkInfo != null && networkInfo.isAvailable
&& networkInfo.type == ConnectivityManager.TYPE_WIFI
}
}
package com.by_syk.onetapcdutnet.util
import java.security.MessageDigest
object MD5 {
/**
* MD5 加密并返回十六进制结果
*/
fun md5(text: String): String {
val messageDigest = MessageDigest.getInstance("MD5")
messageDigest.update(text.toByteArray())
val bytes = messageDigest.digest()
return parseHex(bytes)
}
private fun parseHex(bytes: ByteArray): String {
val sb = StringBuilder()
for (b in bytes) {
val tmp = (b.toInt() and 0xff).toString(16)
if (tmp.length == 1) {
sb.append("0")
}
sb.append(tmp)
}
return sb.toString()
}
}
package com.by_syk.onetapcdutnet.util
object C {
// 校园网登录地址
val CAMPUS_NET_URL = "http://172.20.255.252"
// 登录用户名
val USER_NAME = "xxx"
// 登录密码
val PWD = "xxx"
}
接口调用核心类:
package com.by_syk.onetapcdutnet.util
import java.io.BufferedReader
import java.io.DataOutputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
object CdutNet {
/**
* 检测校园网连接状态
*
* @return null - 未连接到校园网
* false - 已连接校园网但未登录
* true - 已登录校园网
*/
fun check(): Boolean? {
var bufferedReader: BufferedReader? = null
try {
val huc = URL(C.CAMPUS_NET_URL).openConnection() as HttpURLConnection
huc.requestMethod = "GET"
huc.connectTimeout = 3000
huc.readTimeout = 3000
val inputStream = huc.inputStream
bufferedReader = BufferedReader(InputStreamReader(inputStream, "GBK"))
val sbContent = StringBuilder()
var buffer = bufferedReader.readLine()
while (buffer != null) {
sbContent.append(buffer)
buffer = bufferedReader.readLine()
}
return sbContent.contains("已使用时间")
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (bufferedReader != null) {
bufferedReader.close()
}
}
return null
}
/**
* 登录校园网
*
* @param userName 登录帐号
* @param pwd 登录密码
* @return true - 登录成功
* false - 登录失败
*/
fun login(userName: String, pwd: String): Boolean {
try {
val huc = URL(C.CAMPUS_NET_URL).openConnection() as HttpURLConnection
huc.requestMethod = "POST"
huc.connectTimeout = 4000
huc.readTimeout = 4000
huc.doOutput = true
val PID = "1"
val CALG = "12345678"
var enPwd = MD5.md5("$PID$pwd$CALG")
enPwd = "$enPwd$CALG$PID"
val paras = "DDDDD=$userName&upass=$enPwd&R1=0&R2=1¶=00&0MKKey=123456"
val dos = DataOutputStream(huc.outputStream)
dos.write(paras.toByteArray())
dos.flush()
dos.close()
val inputStream = huc.inputStream
val bufferedReader = BufferedReader(InputStreamReader(inputStream, "GBK"))
val sbContent = StringBuilder()
var buffer = bufferedReader.readLine()
while (buffer != null) {
sbContent.append(buffer)
buffer = bufferedReader.readLine()
}
bufferedReader.close()
return sbContent.contains("登录成功窗")
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
}
核心快捷设置图块 Service:
package com.by_syk.onetapcdutnet.service
import android.os.AsyncTask
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.by_syk.onetapcdutnet.R
import com.by_syk.onetapcdutnet.util.C
import com.by_syk.onetapcdutnet.util.CdutNet
import com.by_syk.onetapcdutnet.util.Net
class QSTileService : TileService() {
/**
* 图块可见回调
*/
override fun onStartListening() {
super.onStartListening()
if (Net.isWiFiConnected(this)) {
updateStatus(R.string.tile_label)
} else { // 未连接 WiFi 则将图块置为不可用状态
updateStatus(R.string.tile_status_no_net_conn, false)
}
}
/**
* 图块点击回调
*/
override fun onClick() {
super.onClick()
Task().execute()
}
/**
* 更新图块状态
*
* @param labelId 文字ID
* @param enable 状态
*/
fun updateStatus(labelId: Int?, enable: Boolean = true) {
if (labelId != null) {
qsTile.label = getString(labelId)
}
if (enable) {
qsTile.state = Tile.STATE_ACTIVE
} else {
qsTile.state = Tile.STATE_UNAVAILABLE
}
qsTile.updateTile()
}
/**
* 核心异步任务,检查、登录校园网
*/
inner class Task : AsyncTask() {
override fun doInBackground(vararg params: String?): Boolean {
publishProgress(R.string.tile_status_check)
val res = CdutNet.check() // 检查校园网连接
if (res == null) { // 未连接校园网,结束
publishProgress(R.string.tile_status_not)
return false
} else if (!res) { // 未登录校园网,进行登录操作
publishProgress(R.string.tile_status_loggin)
if (!CdutNet.login(C.USER_NAME, C.PWD)) { // 登录失败,结束
publishProgress(R.string.tile_status_failed)
return false
}
}
// 至此,表示已成功登录
publishProgress(R.string.tile_status_ok)
return true
}
override fun onProgressUpdate(vararg values: Int?) {
super.onProgressUpdate(*values)
updateStatus(values[0])
}
}
}
三、写在最后
完整代码请移步 GitHub 查看:OneTapCdutNet - GitHub