新的学期已经开始, 我的大学生活已经过了一半, 当年不熟悉的事物现在越来越变得习以为常. 但就上网还需要登陆这件事让我感到很是不习惯. 为一个会写程序的人, 怎能不通过程序去简化这一过程呢? 想到这, 我这已经干涸的大脑就好像注入了润滑油一般高速运转起来...

分析登录原理

  连接到校园网, 打开需要浏览的网址, 观察抓包信息:

抓包

  浏览 http://202.108.22.5 时返回的状态码是200, 但也进行了重定向. 那么应该是通过网页代码进行的跳转. 打开 view-source:http://202.108.22.5/ 发现了跳转语句:

202源码

  请求的地址为 http://192.168.1.93/switch.php, 参数如下所示(均为猜测):

名称 描述
wlanuserip 10.6.4.205 DHCP分配的IP地址
wlanacname D6-3 宿舍楼和楼层
ssid 空的...
nasip 10.0.0.63 服务器IP地址
mac 206a8a69a51e 我的MAC地址
t wireless-v2-plain 接入方式
url http://202.108.22.5/ 需要浏览的网址

  访问 http://192.168.1.93/switch.php?wlanuserip=10.6.4.205&wlanacname=D6-3&ssid=&nasip=10.0.0.63&mac=206a8a69a51e&t=wireless-v2-plain&url=http://202.108.22.5/ , 状态码为302.

  跳转到 http://192.168.1.93/portalcloud/page/3/PC/chn/Login.html?wlanuserip=10.6.4.205&wlanacname=D6-3&ssid=&nasip=10.0.0.63&mac=206a8a69a51e&t=wireless-v2-plain&url=http://202.108.22.5/ 的同时, 给我存了一个Cookie:

名称 描述
PHPSESSID 0q560qo79g7osdld27p4lgit01 Session

  现在所显示的就是登录页面了:

login

  输入账号密码, 点击登录, 请求的地址为 http://192.168.1.93/post.php , post参数为:

名称 描述
pageId 3
username ****************** 用户名
password ******** 密码
0MKKey 登录

  顺便, 经过实验, 若无 pageId0MKKey 参数提交, 也可以登录. 但如果没有 PHPSESSID 或 PHPSESSID 错误, 则无法登录.

  提交表单后, 302重定向到 http://192.168.1.93/switch.php?p=status , 再302重定向到 http://192.168.1.93/portalcloud/page/3/PC/chn/Query.html?p=status . 显示"登录成功"页面:

successful

获取网络信息

  我的思路是构造用于获取 PHPSESSID 的连接并访问. 需要知道的信息为本机的 MAC 和 IP 地址.

  在QT中用于获取网络相关信息的类是 QNetworkInterface , 可以获取到以下信息:

  • hardwareAddress() 设备MAC地址.
  • humanReadableName() 设备可读名(如 "本地连接"/ "VMware Network Adapter VMnet1"等).
  • name() 设备名(如 "{29585BF8-EB77-4F50-B75B-47F675BC80B5}" 等).
  • flags() 设备状态, 取值为:
    • IsUp 已经激活.
    • IsRunning 正在运行.
    • CanBroadcast 广播模式.
    • IsLoopBack 虚拟回环接口.
    • IsPointToPoint 点对点.
    • CanMulticast 支持多播.
  • addressEntries() IP地址相关信息, 需要遍历, 每一组数据包含:
    • ip() ip地址.
    • netmask() 子网掩码.
    • broadcast() 广播地址.

  在这里我们需要传入 humanReadableName , 获取MAC地址和IP地址, 于是就写出了这个函数:

AutoLogin::AutoLogin(QString devicename, QObject *parent) : QObject(parent){ //重载构造函数以指定网卡名称
    for (const QNetworkInterface& netinterface : QNetworkInterface::allInterfaces()){ //遍历所有网络信息
        if(netinterface.humanReadableName() == devicename){ //判断设备名
            macaddr = netinterface.hardwareAddress(); //获取MAC地址
            ipv4addr = netinterface.addressEntries()[0].ip().toString(); //获取IP地址
        }
    }
}

  鉴于大多人只有一个网卡, 接一条网线, 设一个IP地址, 我们还可以让这个函数更智能一些:

AutoLogin::AutoLogin(QObject *parent) : QObject(parent){ //默认构造函数
    for (const QNetworkInterface& netinterface : QNetworkInterface::allInterfaces()){ //遍历所有网络信息
        QNetworkInterface::InterfaceFlags flags = netinterface.flags(); //获取网络状态信息
        if (flags.testFlag(QNetworkInterface::IsRunning) && !flags.testFlag(QNetworkInterface::IsLoopBack)){ //正在运行且非回环
            if (netinterface.addressEntries()[0].ip().toString().left(3) == "10.") //如果IP地址以10开头(学校是A类内网地址)
                macaddr = netinterface.hardwareAddress(); //获取MAC地址
                ipv4addr = netinterface.addressEntries()[0].ip().toString(); //获取IP地址
        }
    }
}

  如此, 我们就成功获得了本机的IP地址和MAC地址.

  写到这里我发现自己的思路有点儿不太对, 比起这样通过程序获取我的网络信息并构造连接地址, 不如模仿我们手动上网登录流程, 打开网页的同时从其源码中获取连接地址. 这样就算是有多个网卡多个 IP 或者不在6号楼也可以正常连接.

获取 PHPSESSID

  重整思路, 我应该做的其实是先访问任意网页, 获取其源码, 然后读取到需要跳转的地址并访问.

  在任何操作前, 我们应当先确定网络已经联通:

bool AutoLogin::getConnectionState() { //测试网络连通性
    QTcpSocket tcpClient; //TCP客户端
    tcpClient.abort(); //终止之前的连接并复位
    tcpClient.connectToHost("202.108.22.5", 80); //连接百度ip, 80端口
    if (!tcpClient.waitForConnected(256)) { //如果256毫秒之内不能连接百度
        return (false); //返回false
    }
    return (true); //如果连接成功, 返回true
}

  先初始化网络访问类库并关联信号槽(设置回调函数), 用于获取 "用来获取 PHPSESSID" 的url:

void AutoLogin::initGetUrl() { //初始化获取url
    QNetworkAccessManager* manager = new QNetworkAccessManager(); //实例化网络访问类
    connect (manager, SIGNAL(finished(QNetworkReply*)),
                this, SLOT(onReplyFinished(QNetworkReply*))); //关联信号和槽
    manager->get(QNetworkRequest(QUrl("http://202.108.22.5"))); //访问百度的服务器
}

  用于判断是否已经登录:

bool AutoLogin::getLoginState(QString html) { //获取是否登录信息
    QRegExp* reg = new QRegExp("<title>(.*)</title>"); //匹配<title>标签
    qint8 con = reg->indexIn(html); //获取匹配位置, 若无匹配返回-1
    if (con != -1) { //如果有匹配
        if (reg->cap(1) == "百度一下,你就知道") { //如果匹配值为百度标题
            return (true); //返回true
        }
    }
    return (false); //若无匹配或标题不为"百度一下,你就知道", 返回false
}

  正则匹配出需要访问的连接地址:

QString AutoLogin::getSessionUrl(QString html) { //获取"用来获取Session"的Url
    QRegExp* reg = new QRegExp("self\.location\.href='(.*)'"); //匹配跳转目的页面
    qint8 con = reg->indexIn(html); //获取匹配位置, 若无匹配返回-1
    if (con != -1) { //如果有匹配
        return (reg->cap(1)); //返回匹配到的url
    }
    return (""); //如果没匹配则返回空字符串
}

  然后是关于 Cookie 的操作了, 写的时候发现 QNetworkCookie::allCookies() 是一个 private 函数, 我不得已继承了一下:

//networkcookie.h
#ifndef NETWORKCOOKIE_H
#define NETWORKCOOKIE_H

#include <QNetworkCookie>
#include <QNetworkCookieJar>

class NetworkCookie : public QNetworkCookieJar{
    public:
        NetworkCookie();
        QList<QNetworkCookie> getCookies();
        void setCookies(const QList<QNetworkCookie> &cookieList);
};

#endif // NETWORKCOOKIE_H

//--------------------------------//
//networkcookie.cpp
#include "networkcookie.h"

NetworkCookie::NetworkCookie(){

}

QList<QNetworkCookie> NetworkCookie::getCookies(){
    return (allCookies());
}

void NetworkCookie::setCookies(const QList<QNetworkCookie> &cookieList){
    setAllCookies(cookieList);
}

  OK, 可以外部访问了. 再次实例化网络访问类, 这次是用来获取 PHPSESSID 的.

void AutoLogin::initGetCookies(QString url) { //初始化获取cookie
    QNetworkAccessManager* manager = new QNetworkAccessManager(); //实例化网络访问类
    cookiejar = new NetworkCookie(); //cookie容器
    manager->setCookieJar(cookiejar); //设置网络访问类的cookie容器
    connect (manager, SIGNAL(finished(QNetworkReply*)),
                this, SLOT(onCookieFinished(QNetworkReply*))); //关联信号和槽
    manager->get(QNetworkRequest(QUrl(url))); //访问url
}

  至此 PHPSESSID 就获取到了.

提交 PSOT 请求

  最后就是登录了,

void AutoLogin::initLogin() { //初始化登录
    QNetworkAccessManager* manager = new QNetworkAccessManager(); //实例化网络访问类
    manager->setCookieJar(cookiejar); //设置cookie为之前获取到的cookie
    connect (manager, SIGNAL(finished(QNetworkReply*)),
                this, SLOT(onLoginFinished(QNetworkReply*))); //关联信号和槽
    QNetworkRequest* request = new QNetworkRequest(); //实例化网络请求
    request->setUrl(QUrl(LOGINURL)); //设置请求URL
    request->setHeader(QNetworkRequest::ContentTypeHeader,
                      "application/x-www-form-urlencoded"); //设置请求头
    QByteArray parameter; //post参数
    parameter.append(REGULAR.arg(Identity).arg(Password)); //构造post参数
    manager->post(*request, parameter); //发送post请求
}

  三个槽(回调函数)只需要处理逻辑.

void AutoLogin::onReplyFinished(QNetworkReply* reply) { //百度访问完成回调函数
    if (reply->error() != QNetworkReply::NoError) { //如果请求失败
        std::cout << "Get url error." << std::endl; //提示获取url错误
        emit quit(); //触发关闭程序的信号
        return; //跳出
    }

    QString html = reply->readAll(); //读取获取的html代码
    if (getLoginState(html)) { //如果正常访问百度
        std::cout << "Connected network." << std::endl; //提示已经连接网络
        emit quit(); //触发关闭程序的信号
        return; //跳出
    }

    QString url = getSessionUrl(html); //获取需要跳转的地址
    if (url == "") { //如果没有获取到需要跳转的地址
        std::cout << "Only use in QCHM." << std::endl; //只能用于青岛酒店管理学院
        emit quit(); //触发关闭程序的信号
        return; //跳出
    }

    initGetCookies(url); //初始化获取cookie
}

void AutoLogin::onCookieFinished(QNetworkReply* reply) { //获取cookie完成回调函数
    if (reply->error() != QNetworkReply::NoError) { //如果请求失败
        std::cout << "Get cookies error." << std::endl; //提示获取cookie错误
        emit quit(); //触发关闭程序的信号
        return; //跳出
    }

    std::cout << "Your PHPSESSID is: "
              << cookiejar->getCookies()[0].value().data()
              << std::endl; //输出PHPSESSID信息

    initLogin(); //初始化登录
}

void AutoLogin::onLoginFinished(QNetworkReply* reply) { //登录完成回调函数
    if (reply->error() != QNetworkReply::NoError) { //如果请求失败
        std::cout << "Login error." << std::endl; //提示登录错误
    } else { //如果成功
        if (reply->readAll() == "<!--post ver:1.0.0 -->\n") { //如果有返回值
            std::cout << "Login successful." << std::endl; //提示登录成功
        } else { //如果没有
            std::cout << "Identity or password error." << std::endl; //提示账号密码错误
        }
    }

    emit quit(); //触发关闭程序的信号
    return; //跳出
}

主函数

  主函数的难点也只有信号和槽的关联了, 由于没有继承自 QObject , 所以 connect 操作需要使用 QObject::connect 调用. AutoLogin 中的 quit() 不要忘记声明为 SIGNALS .

#include <QCoreApplication> //Qt核心
#include <autologin.h> //自己写的类库

int main(int argc, char *argv[]) //主函数
{
    QCoreApplication a(argc, argv); //Qt核心类库

    AutoLogin *al = new AutoLogin(); //实例化自己写的类库
    QObject::connect(al, SIGNAL(quit()), //信号是我类库中的quit()信号
                     &a, SLOT(quit())); //槽是Qt核心退出
    al->setIdentity(argv[1]); //把用户名作为第一个参数传入
    al->setPassword(argv[2]); //把密码作为第二个参数传入
    al->autoLogin(); //开始登录

    return a.exec(); //进入Qt事件循环
}

  顺便提及一下 main 函数中的两个形参:

参数名 意义
argc 程序运行时传入参数的个数
argv[0] 当前程序的绝对路径
argv[1]及以后 调用此程序时输入的参数

设置开机启动

  我在众多 Linux 发行版中选择 LinuxMint 作为我主力办公机系统, 也是不无原因的. 不仅因它基于 Ubuntu(Debian) , 有着庞大的社区支持, 非常方便开发者进行软件开发. 而且还有简洁的桌面, 丰富的内置应用. 也是我选择它的原因. 对我这种 Linux 小白而言, 在需要快速实现某项功能却不知道敲什么命令的时候, 给出一个简介强大的界面. 简直不能太赞.

  编译完成后把可执行文件复制到适当的目录. 添加一个新的开机启动项, 命令设置为 /home/moe8023/Program/AutoLogin/AutoLogin ****************** ******** , 前部分的 * 表示我的账号(身份证号), 后部分是密码.

start

结语

  如此, 我的计算机只要一开机, 不出意料10秒后就自动登录了校园网. 虽说自个手动打开浏览器登录也不是什么费事儿的事. ~~但可以给博客凑数, 跟同学装逼啊~~ 但时不时写写程序, 记记笔记打发一下无聊的时间, 也是有益身心健康的.

  最后附上项目的 Github 和 Coding 地址, 如果有需要的, 可以自行下载. 如果有什么好的意见和建议, 可以在这篇文章下面留言, 也可以在 Github 或 Coding 提交 issues.